hOpenPGP 3.0.2.2 → 3.1
raw patch · 26 files changed
+35249/−27945 lines, 26 files
Files
- Codec/Encryption/OpenPGP/Arbitrary.hs +324/−299
- Codec/Encryption/OpenPGP/Encrypt.hs +2932/−2415
- Codec/Encryption/OpenPGP/Internal.hs +133/−106
- Codec/Encryption/OpenPGP/Internal/CryptoECDH.hs +65/−51
- Codec/Encryption/OpenPGP/KeyInfo.hs +24/−18
- Codec/Encryption/OpenPGP/KeyringParser.hs +537/−445
- Codec/Encryption/OpenPGP/Message.hs +1076/−890
- Codec/Encryption/OpenPGP/Policy.hs +471/−315
- Codec/Encryption/OpenPGP/Serialize.hs +3617/−3127
- Codec/Encryption/OpenPGP/Signatures.hs +2404/−1766
- Codec/Encryption/OpenPGP/Types.hs +3/−3
- Codec/Encryption/OpenPGP/Types/Internal/Base.hs +2050/−1767
- Codec/Encryption/OpenPGP/Types/Internal/PKITypes.hs +353/−344
- Codec/Encryption/OpenPGP/Types/Internal/PacketClass.hs +435/−379
- Codec/Encryption/OpenPGP/Types/Internal/Pkt.hs +488/−371
- Data/Conduit/OpenPGP/Decrypt.hs +2924/−2478
- Data/Conduit/OpenPGP/Keyring.hs +374/−368
- Data/Conduit/OpenPGP/Verify.hs +112/−88
- bench/mark.hs +103/−84
- hOpenPGP.cabal +2/−2
- tests/Tests/Common.hs +1924/−1534
- tests/Tests/Encryption.hs +7233/−5087
- tests/Tests/Keys.hs +2245/−1768
- tests/Tests/MessageAndArmor.hs +2637/−1937
- tests/Tests/Serialization.hs +1164/−952
- tests/Tests/Utilities.hs +1619/−1351
Codec/Encryption/OpenPGP/Arbitrary.hs view
@@ -2,379 +2,404 @@ -- Copyright © 2014-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).- {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleInstances #-} module Codec.Encryption.OpenPGP.Arbitrary- (- ) where+ (+ ) where -import Codec.Encryption.OpenPGP.Types import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import qualified Data.List.NonEmpty as NE import Data.Maybe (fromMaybe) import Network.URI (nullURI, parseURI) import Test.QuickCheck- ( Arbitrary(..)- , Gen- , choose- , elements- , frequency- , getPositive- , listOf1- , oneof- , sized- , suchThat- , vector- )+ ( Arbitrary (..)+ , Gen+ , choose+ , elements+ , frequency+ , getPositive+ , listOf1+ , oneof+ , sized+ , suchThat+ , vector+ ) import Test.QuickCheck.Instances () +import Codec.Encryption.OpenPGP.Types+ instance Arbitrary (PKESK 'PKESKV3) where- arbitrary = do- eoki <- arbitrary- pka <- arbitrary- mpis <-- case pka of- RSA -> (\mpi -> mpi NE.:| []) <$> arbitrary- DeprecatedRSAEncryptOnly -> (\mpi -> mpi NE.:| []) <$> arbitrary- ElgamalEncryptOnly -> do- mpi1 <- arbitrary- mpi2 <- arbitrary- pure (mpi1 NE.:| [mpi2])- ForbiddenElgamal -> do- mpi1 <- arbitrary- mpi2 <- arbitrary- pure (mpi1 NE.:| [mpi2])- ECDH -> do- mpi1 <- arbitrary- mpi2 <- arbitrary- pure (mpi1 NE.:| [mpi2])- _ -> arbitrary- pure (PKESK3Packet 3 eoki pka mpis)+ arbitrary = do+ eoki <- arbitrary+ pka <- arbitrary+ mpis <-+ case pka of+ RSA -> (\mpi -> mpi NE.:| []) <$> arbitrary+ DeprecatedRSAEncryptOnly -> (\mpi -> mpi NE.:| []) <$> arbitrary+ ElgamalEncryptOnly -> do+ mpi1 <- arbitrary+ mpi2 <- arbitrary+ pure (mpi1 NE.:| [mpi2])+ ForbiddenElgamal -> do+ mpi1 <- arbitrary+ mpi2 <- arbitrary+ pure (mpi1 NE.:| [mpi2])+ ECDH -> do+ mpi1 <- arbitrary+ mpi2 <- arbitrary+ pure (mpi1 NE.:| [mpi2])+ _ -> arbitrary+ pure (PKESK3Packet 3 eoki pka mpis) instance Arbitrary (PKESK 'PKESKV6) where- arbitrary = do- rid <-- oneof- [ pure BL.empty- , BL.pack <$> vector 20- , BL.pack . (4 :) <$> vector 20- , BL.pack <$> vector 32- , BL.pack . (6 :) <$> vector 32- ]- pka <- arbitrary- esk <- BL.pack <$> listOf1 arbitrary- pure (PKESK6Packet rid pka esk)+ arbitrary = do+ rid <-+ oneof+ [ pure BL.empty+ , BL.pack <$> vector 20+ , BL.pack . (4 :) <$> vector 20+ , BL.pack <$> vector 32+ , BL.pack . (6 :) <$> vector 32+ ]+ pka <- arbitrary+ esk <- BL.pack <$> listOf1 arbitrary+ pure (PKESK6Packet rid pka esk) instance Arbitrary (SKESK 'SKESKV4) where- arbitrary = do- sa <- elements [AES128, AES192, AES256]- s2k <- arbitrarySKESKv4S2K- esk <- oneof [pure Nothing, Just . BL.pack <$> listOf1 arbitrary]- pure (SKESK4Packet sa s2k esk)+ arbitrary = do+ sa <- elements [AES128, AES192, AES256]+ s2k <- arbitrarySKESKv4S2K+ esk <- oneof [pure Nothing, Just . BL.pack <$> listOf1 arbitrary]+ pure (SKESK4Packet sa s2k esk) instance Arbitrary (SKESK 'SKESKV6) where- arbitrary = do- sa <- elements [AES128, AES192, AES256]- aead <- elements [EAX, OCB, GCM]- s2k <- arbitrarySKESKv6S2K- iv <- BL.pack <$> vector 16- esk <- BL.pack <$> listOf1 arbitrary- tag <- BL.pack <$> vector 16- pure (SKESK6Packet sa aead s2k iv esk tag)+ arbitrary = do+ sa <- elements [AES128, AES192, AES256]+ aead <- elements [EAX, OCB, GCM]+ s2k <- arbitrarySKESKv6S2K+ iv <- BL.pack <$> vector 16+ esk <- BL.pack <$> listOf1 arbitrary+ tag <- BL.pack <$> vector 16+ pure (SKESK6Packet sa aead s2k iv esk tag) arbitrarySKESKv4S2K :: Gen S2K arbitrarySKESKv4S2K =- oneof- [ Simple <$> elements supportedSKESKHashAlgorithms- , Salted <$> elements supportedSKESKHashAlgorithms <*> (Salt8 . B.pack <$> vector 8)- ]+ oneof+ [ Simple <$> elements supportedSKESKHashAlgorithms+ , Salted+ <$> elements supportedSKESKHashAlgorithms+ <*> (Salt8 . B.pack <$> vector 8)+ ] arbitrarySKESKv6S2K :: Gen S2K arbitrarySKESKv6S2K =- Argon2 <$>- (Salt16 . B.pack <$> vector 16) <*>- choose (1, 4) <*>- choose (1, 4) <*>- choose (3, 31)+ Argon2+ <$> (Salt16 . B.pack <$> vector 16)+ <*> choose (1, 4)+ <*> choose (1, 4)+ <*> choose (3, 31) supportedSKESKHashAlgorithms :: [HashAlgorithm] supportedSKESKHashAlgorithms = [SHA1, SHA256, SHA384, SHA512] instance Arbitrary Signature where- arbitrary = fmap Signature arbitrary+ arbitrary = fmap Signature arbitrary instance Arbitrary UserId where- arbitrary = fmap UserId arbitrary+ arbitrary = fmap UserId arbitrary -- instance Arbitrary SignaturePayload where- arbitrary = frequency [(2, three), (3, four), (1, other)]- where- three = do- st <- arbitrary- w32 <- arbitrary- eoki <- arbitrary- pka <- arbitrary- ha <- arbitrary- w16 <- arbitrary- SigV3 st w32 eoki pka ha w16 <$> arbitrary- four = do- st <- arbitrary- pka <- arbitrary- ha <- arbitrary- has <- arbitrary- uhas <- arbitrary- w16 <- arbitrary- SigV4 st pka ha has uhas w16 <$> arbitrary- other = do- v <- oneof [pure 5, choose (7, maxBound)]- SigVOther v <$> arbitrary+ arbitrary = frequency [(2, three), (3, four), (1, other)]+ where+ three = do+ st <- arbitrary+ w32 <- arbitrary+ eoki <- arbitrary+ pka <- arbitrary+ ha <- arbitrary+ w16 <- arbitrary+ SigV3 st w32 eoki pka ha w16 <$> arbitrary+ four = do+ st <- arbitrary+ pka <- arbitrary+ ha <- arbitrary+ has <- arbitrary+ uhas <- arbitrary+ w16 <- arbitrary+ SigV4 st pka ha has uhas w16 <$> arbitrary+ other = do+ v <- oneof [pure 5, choose (7, maxBound)]+ SigVOther v <$> arbitrary instance Arbitrary SigSubPacket where- arbitrary = do- crit <- arbitrary- SigSubPacket crit <$> arbitrary+ arbitrary = do+ crit <- arbitrary+ SigSubPacket crit <$> arbitrary instance Arbitrary SigSubPacketPayload where- arbitrary =- sized $ \n ->- oneof- ([ sct- , set- , ec- , ts- , re- , ket- , psa- , rk- , i- , nd- , phas- , pcas- , ksps- , pks- , puid- , purl- , kfs- , suid- , rfr- , fs- , st- , udss- , oss- , ifp- ] ++- [es n | n > 0])- where- sct = fmap SigCreationTime arbitrary- set = fmap SigExpirationTime arbitrary- ec = fmap ExportableCertification arbitrary- ts =- arbitrary >>= \tl -> arbitrary >>= \ta -> return (TrustSignature tl ta)- re = fmap RegularExpression arbitrary- ket = fmap KeyExpirationTime arbitrary- psa = fmap PreferredSymmetricAlgorithms arbitrary- rk =- arbitrary >>= \rcs ->- arbitrary >>= \pka ->- fmap (RevocationKey rcs pka . Fingerprint . BL.pack) (vector 20)- i = fmap Issuer arbitrary- nd =- arbitrary >>= \nfs ->- arbitrary >>= \nn ->- arbitrary >>= \nv -> return (NotationData nfs nn nv)- phas = fmap PreferredHashAlgorithms arbitrary- pcas = fmap PreferredCompressionAlgorithms arbitrary- ksps = fmap KeyServerPreferences arbitrary- pks = fmap PreferredKeyServer arbitrary- puid = fmap PrimaryUserId arbitrary- purl = fmap (PolicyURL . URL . fromMaybe nullURI . parseURI) arbitrary- kfs = fmap KeyFlags arbitrary- suid = fmap SignersUserId arbitrary- rfr =- arbitrary >>= \rc ->- arbitrary >>= \rr -> return (ReasonForRevocation rc rr)- fs = fmap Features arbitrary- st =- arbitrary >>= \pka ->- arbitrary >>= \ha ->- arbitrary >>= \sh -> return (SignatureTarget pka ha sh)- es _ = pure (EmbeddedSignature (SigVOther 5 BL.empty))- ifp =- elements [IssuerFingerprintV4, IssuerFingerprintV6] >>= \v ->- fmap- (IssuerFingerprint v)- (if v == IssuerFingerprintV6- then fmap (Fingerprint . BL.pack) (vector 32)- else fmap (Fingerprint . BL.pack) (vector 20))- udss =- choose (100, 110) >>= \a ->- arbitrary >>= \b -> return (UserDefinedSigSub a b)- oss =- suchThat arbitrary isOtherSigSubType >>= \a ->- arbitrary >>= \b -> return (OtherSigSub a b)- isOtherSigSubType a =- a <= 127 &&- a `notElem`- [ 2- , 3- , 4- , 5- , 6- , 7- , 9- , 11- , 12- , 16- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- ] &&- (a < 100 || a > 110)+ arbitrary =+ sized $ \n ->+ oneof+ ( [ sct+ , set+ , ec+ , ts+ , re+ , ket+ , psa+ , rk+ , i+ , nd+ , phas+ , pcas+ , ksps+ , pks+ , puid+ , purl+ , kfs+ , suid+ , rfr+ , fs+ , st+ , udss+ , oss+ , ifp+ , irfp+ , pacs+ ]+ ++ [es n | n > 0]+ )+ where+ sct = fmap SigCreationTime arbitrary+ set = fmap SigExpirationTime arbitrary+ ec = fmap ExportableCertification arbitrary+ ts =+ arbitrary >>= \tl -> arbitrary >>= \ta -> return (TrustSignature tl ta)+ re = fmap RegularExpression arbitrary+ ket = fmap KeyExpirationTime arbitrary+ psa = fmap PreferredSymmetricAlgorithms arbitrary+ rk =+ arbitrary >>= \rcs ->+ arbitrary >>= \pka ->+ fmap (RevocationKey rcs pka . Fingerprint . BL.pack) (vector 20)+ i = fmap Issuer arbitrary+ nd =+ arbitrary >>= \nfs ->+ arbitrary >>= \nn ->+ arbitrary >>= \nv -> return (NotationData nfs nn nv)+ phas = fmap PreferredHashAlgorithms arbitrary+ pcas = fmap PreferredCompressionAlgorithms arbitrary+ ksps = fmap KeyServerPreferences arbitrary+ pks = fmap PreferredKeyServer arbitrary+ puid = fmap PrimaryUserId arbitrary+ purl =+ fmap (PolicyURL . URL . fromMaybe nullURI . parseURI) arbitrary+ kfs = fmap KeyFlags arbitrary+ suid = fmap SignersUserId arbitrary+ rfr =+ arbitrary >>= \rc ->+ arbitrary >>= \rr -> return (ReasonForRevocation rc rr)+ fs = fmap Features arbitrary+ st =+ arbitrary >>= \pka ->+ arbitrary >>= \ha ->+ arbitrary >>= \sh -> return (SignatureTarget pka ha sh)+ es _ = pure (EmbeddedSignature (SigVOther 5 BL.empty))+ ifp =+ elements [IssuerFingerprintV4, IssuerFingerprintV6] >>= \v ->+ fmap+ (IssuerFingerprint v)+ ( if v == IssuerFingerprintV6+ then fmap (Fingerprint . BL.pack) (vector 32)+ else fmap (Fingerprint . BL.pack) (vector 20)+ )+ irfp =+ elements [IssuerFingerprintV4, IssuerFingerprintV6] >>= \v ->+ fmap+ (IntendedRecipient v)+ ( if v == IssuerFingerprintV6+ then fmap (Fingerprint . BL.pack) (vector 32)+ else fmap (Fingerprint . BL.pack) (vector 20)+ )+ pacs = fmap PreferredAEADCiphersuites arbitrary+ udss =+ choose (100, 110) >>= \a ->+ arbitrary >>= \b -> return (UserDefinedSigSub a b)+ oss =+ suchThat arbitrary isOtherSigSubType >>= \a ->+ arbitrary >>= \b -> return (OtherSigSub a b)+ isOtherSigSubType a =+ a <= 127+ && a+ `notElem` [ 2+ , 3+ , 4+ , 5+ , 6+ , 7+ , 9+ , 11+ , 12+ , 16+ , 20+ , 21+ , 22+ , 23+ , 24+ , 25+ , 26+ , 27+ , 28+ , 29+ , 30+ , 31+ , 32+ , 33+ , 35+ , 39+ ]+ && (a < 100 || a > 110) -- instance Arbitrary PubKeyAlgorithm where- arbitrary = elements [RSA, DSA, ECDH, ECDSA, DH, EdDSA]+ arbitrary = elements [RSA, DSA, ECDH, ECDSA, DH, EdDSA] instance Arbitrary EightOctetKeyId where- arbitrary = fmap (EightOctetKeyId . BL.pack) (vector 8)+ arbitrary = fmap (EightOctetKeyId . BL.pack) (vector 8) instance Arbitrary Fingerprint where- arbitrary =- oneof- [ fmap (Fingerprint . BL.pack) (vector 16) -- v3- , fmap (Fingerprint . BL.pack) (vector 20) -- v4- , fmap (Fingerprint . BL.pack) (vector 32) -- v6- ]+ arbitrary =+ oneof+ [ fmap (Fingerprint . BL.pack) (vector 16) -- v3+ , fmap (Fingerprint . BL.pack) (vector 20) -- v4+ , fmap (Fingerprint . BL.pack) (vector 32) -- v6+ ] instance Arbitrary MPI where- arbitrary = fmap (MPI . getPositive) arbitrary+ arbitrary = fmap (MPI . getPositive) arbitrary instance Arbitrary SigType where- arbitrary =- elements- [ BinarySig- , CanonicalTextSig- , StandaloneSig- , GenericCert- , PersonaCert- , CasualCert- , PositiveCert- , SubkeyBindingSig- , PrimaryKeyBindingSig- , SignatureDirectlyOnAKey- , KeyRevocationSig- , SubkeyRevocationSig- , CertRevocationSig- , TimestampSig- , ThirdPartyConfirmationSig- ]+ arbitrary =+ elements+ [ BinarySig+ , CanonicalTextSig+ , StandaloneSig+ , GenericCert+ , PersonaCert+ , CasualCert+ , PositiveCert+ , SubkeyBindingSig+ , PrimaryKeyBindingSig+ , SignatureDirectlyOnAKey+ , KeyRevocationSig+ , SubkeyRevocationSig+ , CertRevocationSig+ , TimestampSig+ , ThirdPartyConfirmationSig+ ] instance Arbitrary HashAlgorithm where- arbitrary =- elements- [ DeprecatedMD5- , SHA1- , RIPEMD160- , SHA256- , SHA384- , SHA512- , SHA224- , SHA3_256- , SHA3_512- ]+ arbitrary =+ elements+ [ DeprecatedMD5+ , SHA1+ , RIPEMD160+ , SHA256+ , SHA384+ , SHA512+ , SHA224+ , SHA3_256+ , SHA3_512+ ] instance Arbitrary SymmetricAlgorithm where- arbitrary =- elements- [ Plaintext- , IDEA- , TripleDES- , CAST5- , Blowfish- , ReservedSAFER- , ReservedDES- , AES128- , AES192- , AES256- , Twofish- , Camellia128- , Camellia192- , Camellia256- ]+ arbitrary =+ elements+ [ Plaintext+ , IDEA+ , TripleDES+ , CAST5+ , Blowfish+ , ReservedSAFER+ , ReservedDES+ , AES128+ , AES192+ , AES256+ , Twofish+ , Camellia128+ , Camellia192+ , Camellia256+ ] +instance Arbitrary AEADAlgorithm where+ arbitrary = elements [EAX, OCB, GCM]+ instance Arbitrary RevocationClass where- arbitrary = frequency [(9, srk), (1, rco)]- where- srk = return SensitiveRK- rco = fmap mkRevocationClass (choose (2, 6))+ arbitrary = frequency [(9, srk), (1, rco)]+ where+ srk = return SensitiveRK+ rco = fmap mkRevocationClass (choose (2, 6)) instance Arbitrary NotationFlag where- arbitrary = frequency [(9, hr), (1, onf)]- where- hr = return HumanReadable- onf = fmap mkNotationFlag (choose (1, 15))+ arbitrary = frequency [(9, hr), (1, onf)]+ where+ hr = return HumanReadable+ onf = fmap mkNotationFlag (choose (1, 15)) instance Arbitrary CompressionAlgorithm where- arbitrary = elements [Uncompressed, ZIP, ZLIB, BZip2]+ arbitrary = elements [Uncompressed, ZIP, ZLIB, BZip2] instance Arbitrary KSPFlag where- arbitrary = frequency [(9, nm), (1, kspo)]- where- nm = return NoModify- kspo = fmap KSPOther (choose (2, 63))+ arbitrary = frequency [(9, nm), (1, kspo)]+ where+ nm = return NoModify+ kspo = fmap KSPOther (choose (2, 63)) instance Arbitrary KeyFlag where- arbitrary =- elements- [ GroupKey- , AuthKey- , SplitKey- , EncryptStorageKey- , EncryptCommunicationsKey- , SignDataKey- , CertifyKeysKey- ]+ arbitrary =+ elements+ [ GroupKey+ , AuthKey+ , SplitKey+ , EncryptStorageKey+ , EncryptCommunicationsKey+ , SignDataKey+ , CertifyKeysKey+ ] instance Arbitrary RevocationCode where- arbitrary =- elements- [ NoReason- , KeySuperseded- , KeyMaterialCompromised- , KeyRetiredAndNoLongerUsed- , UserIdInfoNoLongerValid- ]+ arbitrary =+ elements+ [ NoReason+ , KeySuperseded+ , KeyMaterialCompromised+ , KeyRetiredAndNoLongerUsed+ , UserIdInfoNoLongerValid+ ] instance Arbitrary FeatureFlag where- arbitrary = frequency [(8, seipdV1), (1, seipdV2), (1, fo)]- where- seipdV1 = return FeatureSEIPDv1- seipdV2 = return FeatureSEIPDv2- fo = fmap FeatureOther (suchThat (choose (0, 63)) (`notElem` [4, 7]))+ arbitrary = frequency [(8, seipdV1), (1, seipdV2), (1, fo)]+ where+ seipdV1 = return FeatureSEIPDv1+ seipdV2 = return FeatureSEIPDv2+ fo =+ fmap FeatureOther (suchThat (choose (0, 63)) (`notElem` [4, 7])) instance Arbitrary ThirtyTwoBitTimeStamp where- arbitrary = fmap ThirtyTwoBitTimeStamp arbitrary+ arbitrary = fmap ThirtyTwoBitTimeStamp arbitrary instance Arbitrary ThirtyTwoBitDuration where- arbitrary = fmap ThirtyTwoBitDuration arbitrary+ arbitrary = fmap ThirtyTwoBitDuration arbitrary instance Arbitrary NotationName where- arbitrary = fmap NotationName arbitrary+ arbitrary = fmap NotationName arbitrary instance Arbitrary NotationValue where- arbitrary = fmap NotationValue arbitrary+ arbitrary = fmap NotationValue arbitrary++instance Arbitrary Padding where+ arbitrary = fmap Padding arbitrary
Codec/Encryption/OpenPGP/Encrypt.hs view
@@ -2,2418 +2,2935 @@ -- Copyright © 2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).--{-# LANGUAGE DataKinds #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE PackageImports #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE StandaloneDeriving #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeApplications #-}--module Codec.Encryption.OpenPGP.Encrypt- ( PKESKEncryptError(..)- , RecipientCapabilityNegotiationMode(..)- , RecipientCapabilityError(..)- , renderRecipientCapabilityError- , RecipientCapabilities(..)- , recipientCapabilitiesFromSubpacketPayloads- , recipientCapabilitySupportsEncryption- , RecipientTargetRejectionReason(..)- , RecipientEncryptionTargetRejected(..)- , RecipientEncryptionTargetsReport(..)- , recipientEncryptionTargetsReportFromTKAtTimestamp- , recipientEncryptionTargetsReportFromTK- , recipientEncryptionTargetFromTKAtTimestamp- , recipientEncryptionTargetFromTKAtTimestampWithPolicy- , recipientEncryptionTargetsFromTKAtTimestamp- , recipientEncryptionTargetFromTK- , recipientEncryptionTargetFromTKWithPolicy- , recipientEncryptionTargetsFromTK- , RecipientTargetSelectionPolicy(..)- , PassphraseSKESKVersionPolicy(..)- , PassphraseEncryptRequest(..)- , encryptPassphraseWithPolicy- , PKESKVersionPolicy(..)- , RecipientPKESKVersionStrategy(..)- , RecipientPKESKVersionStrategyW(..)- , SomeRecipientPKESKVersionStrategyW(..)- , RecipientPKESKVersionSelector- , RecipientPKESKVersionSelectorTyped- , EncryptCompatibilityProfile(..)- , EncryptCompatibilityProfileW(..)- , SomeEncryptCompatibilityProfileW(..)- , RecipientEncryptionTarget(..)- , recipientEncryptionTarget- , recipientEncryptionTargetWithStrategy- , recipientEncryptionTargetWithCapabilities- , recipientEncryptionTargetWithStrategyTyped- , recipientVersionStrategyForProfile- , recipientVersionStrategyForProfileTyped- , RecipientPayloadShape(..)- , defaultRecipientPayloadShape- , SEIPDVersion(..)- , RecipientEncryptResult(..)- , RecipientEncryptRequest(..)- , RecipientEncryptRequestOverrides(..)- , encryptForRecipients- , encryptForRecipientsLegacy- , encryptForRecipientsWithCapabilityNegotiation- , PKESKV3SessionMaterial- , PKESKV6RawSessionMaterial- , PKESKSessionMaterial- , pkeskSessionAlgorithm- , pkeskSessionKey- , mkPKESKSessionMaterial- , mkPKESKV3SessionMaterial- , mkPKESKV6RawSessionMaterial- , pkeskV3SessionMaterial- , pkeskV6RawSessionMaterial- , encodeOpenPGPSessionMaterial- , generateSessionKeyMaterial- , canonicalizePKESKRecipientId- , canonicalizePKESKPacketRecipientIds- , buildPKESKv3PayloadForRecipient- , buildPKESKv3PktForRecipient- , buildPKESKPayloadForRecipient- , buildPKESKPktForRecipient- , buildPKESKPktsForRecipientTargetsWithSelector- , buildPKESKPktsForRecipientTargetsWithSelectorTyped- , encryptSEIPDv2Payload- , encryptSEIPDv1Payload- , encryptSEIPDv2WithSKESK- , encryptSEIPDv2WithSKESKBlock- , encryptSEIPDv2LiteralDataWithSKESK- , composeMessageWithSEIPDv2- ) where--import Codec.Encryption.OpenPGP.BlockCipher (CipherError, renderCipherError, keySize, withSymmetricCipher)-import Codec.Encryption.OpenPGP.CFB- ( OpenPGPCFBModeW(..)- , encryptOpenPGPCfbRaw- , mdcTrailerForSEIPDv1- )-import Codec.Encryption.OpenPGP.Internal.HOBlockCipher (HOBlockCipher(..))-import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)-import Codec.Encryption.OpenPGP.Internal (leftPadTo, point2MBS)-import Codec.Encryption.OpenPGP.Internal.CryptoAES (withAESCipher)-import Codec.Encryption.OpenPGP.Internal.CryptoECDH- ( normalizeMontgomeryPublic- , buildECDHKDFParam- , deriveECDHKek- )-import Codec.Encryption.OpenPGP.Internal.CryptoSEIPDv2- ( aeadModeAndNonceSizeForSEIPDv2- , deriveSKESK6KEK- , encryptSKESK6SessionKey- , seipdv2SymmetricKeySize- )-import Codec.Encryption.OpenPGP.Policy- ( PKESKVersionPolicy(..)- , OpenPGPRFC(..)- , MessageEncryptionPolicy- , policyForRFC- , policyMessageEncryption- , messageDefaultSymmetricAlgorithm- , messageSEIPDv2SymmetricAlgorithms- , messageDefaultAEADAlgorithm- , messageDefaultChunkSize- , messageSEIPDv2SaltOctets- , defaultPKESKVersionPolicy- )-import Codec.Encryption.OpenPGP.Expirations- ( effectiveKeyPreferencesAtTimestamp- , isPKTimeValidWithSelfSignatures- , keyStateAt- , keyStateValid- , signatureEffectiveAt- )-import Codec.Encryption.OpenPGP.Ontology (isSubkeyBindingSig, isSubkeyRevocation)-import Codec.Encryption.OpenPGP.SignatureQualities (sigCT, signatureHashedSubpacketsKnown)-import Codec.Encryption.OpenPGP.Serialize ()-import Codec.Encryption.OpenPGP.S2K (renderS2KError, string2Key)-import Codec.Encryption.OpenPGP.Types-import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as BTypes-import Codec.Encryption.OpenPGP.Internal.RFC7253OCB (encryptWithOCBRFC7253)-import Control.Applicative ((<|>))-import Control.Lens ((.~), ix)-import Control.Monad (when)-import Data.List (find, foldl', maximumBy)-import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)-import Data.Ord (comparing)-import Data.Time.Clock (UTCTime)-import Data.Time.Clock.POSIX (posixSecondsToUTCTime)-import qualified "crypton" Crypto.Cipher.Types as CCT-import qualified Crypto.Error as CE-import qualified Crypto.Hash.Algorithms as CHAlg-import Crypto.KDF.HKDF (expand, extract)-import Crypto.Number.Serialize (i2osp, os2ip)-import qualified Crypto.PubKey.Curve25519 as C25519-import qualified Crypto.PubKey.Curve448 as C448-import qualified Crypto.PubKey.ECC.DH as ECCDH-import qualified Crypto.PubKey.ECC.ECDSA as ECDSA-import qualified Crypto.PubKey.ECC.Generate as ECCGen-import qualified Crypto.PubKey.RSA.PKCS15 as RSA15-import Crypto.Random.Types (MonadRandom, getRandomBytes)-import Data.Binary (put)-import qualified Data.ByteArray as BA-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import Data.Bits ((.&.), shiftL, shiftR, xor)-import Data.Binary.Put (putWord64be, runPut)-import Data.Bifunctor (first)-import Data.List.NonEmpty (NonEmpty(..))-import qualified Data.Set as Set-import Data.Word (Word8, Word16, Word64)-import Data.Int (Int64)---- | Typed failures from one-pass signature packet construction.-data OPSBuildError- = OPSBuildMissingIssuerKeyId- | OPSBuildMissingIssuerFingerprint- | OPSBuildFingerprintWrongLength Int64- | OPSBuildUnsupportedSigVersion PacketVersion- deriving (Eq, Show)--renderOPSBuildError :: OPSBuildError -> String-renderOPSBuildError OPSBuildMissingIssuerKeyId =- "cannot build OPS3 packet from v4 signature without issuer metadata"-renderOPSBuildError OPSBuildMissingIssuerFingerprint =- "cannot build OPS6 packet from v6 signature without issuer fingerprint"-renderOPSBuildError (OPSBuildFingerprintWrongLength n) =- "cannot build OPS6 packet: issuer fingerprint must be 32 octets, got " ++ show n-renderOPSBuildError (OPSBuildUnsupportedSigVersion v) =- "cannot build one-pass signature packet for unsupported signature version " ++ show v---- | Typed failures surfaced by encrypt-side PKESK and SEIPD-v2 helpers.-data PKESKEncryptError- = UnsupportedSessionKeyAlgorithm SymmetricAlgorithm String- | InvalidSessionKeyLength SymmetricAlgorithm Int Int- | InvalidRecipientIdentifier String- | UnsupportedRecipientAlgorithm PubKeyAlgorithm- | InvalidRecipientKeyMaterial PubKeyAlgorithm String- | RecipientKdfFailure PubKeyAlgorithm String- | RecipientKeyWrapFailure PubKeyAlgorithm String- | RecipientCapabilitySelectionFailure RecipientCapabilityError- | PayloadBuildFailure String- | NoRecipientsProvided- deriving (Eq, Show)--renderPKESKEncryptError :: PKESKEncryptError -> String-renderPKESKEncryptError (UnsupportedSessionKeyAlgorithm algo reason) =- "unsupported session key algorithm " ++ show algo ++ ": " ++ reason-renderPKESKEncryptError (InvalidSessionKeyLength algo expected actual) =- "invalid session key length for " ++ show algo ++ ": expected " ++ show expected ++ ", got " ++ show actual-renderPKESKEncryptError (InvalidRecipientIdentifier reason) =- "invalid recipient identifier: " ++ reason-renderPKESKEncryptError (UnsupportedRecipientAlgorithm algo) =- "unsupported recipient public-key algorithm: " ++ show algo-renderPKESKEncryptError (InvalidRecipientKeyMaterial algo reason) =- "invalid recipient key material for " ++ show algo ++ ": " ++ reason-renderPKESKEncryptError (RecipientKdfFailure algo reason) =- "KDF failure for recipient algorithm " ++ show algo ++ ": " ++ reason-renderPKESKEncryptError (RecipientKeyWrapFailure algo reason) =- "key wrap failure for recipient algorithm " ++ show algo ++ ": " ++ reason-renderPKESKEncryptError (RecipientCapabilitySelectionFailure err) =- renderRecipientCapabilityError err-renderPKESKEncryptError (PayloadBuildFailure reason) =- "payload build failure: " ++ reason-renderPKESKEncryptError NoRecipientsProvided =- "no recipients provided"--data RecipientCapabilityNegotiationMode- = RecipientCapabilityNegotiationOff- | RecipientCapabilityNegotiationOn- deriving (Eq, Show)--data RecipientCapabilityError- = RecipientCapabilityMissingEncryptionFlags SomePKPayload (Set.Set KeyFlag)- | RecipientCapabilityNoEncryptableKeyMaterialInTK- | RecipientCapabilityMissingSEIPDv1Support [SomePKPayload]- | RecipientCapabilityMissingSEIPDv2Support [SomePKPayload]- | RecipientCapabilityNoCommonSymmetricAlgorithms [SymmetricAlgorithm]- | RecipientCapabilityNoCommonAEADAlgorithms [AEADAlgorithm]- deriving (Eq, Show)--renderRecipientCapabilityError :: RecipientCapabilityError -> String-renderRecipientCapabilityError (RecipientCapabilityMissingEncryptionFlags recipient flags) =- "recipient " ++ show (_keyVersion recipient, _pkalgo recipient) ++- " does not advertise encryption-capable key flags; observed flags: " ++- show (Set.toList flags)-renderRecipientCapabilityError RecipientCapabilityNoEncryptableKeyMaterialInTK =- "no encryption-capable primary key or subkey was found in transferable key material"-renderRecipientCapabilityError (RecipientCapabilityMissingSEIPDv1Support recipients) =- "recipient set does not advertise SEIPDv1 (MDC) support: " ++- show (map (\r -> (_keyVersion r, _pkalgo r)) recipients)-renderRecipientCapabilityError (RecipientCapabilityMissingSEIPDv2Support recipients) =- "recipient set does not advertise SEIPDv2 support: " ++- show (map (\r -> (_keyVersion r, _pkalgo r)) recipients)-renderRecipientCapabilityError (RecipientCapabilityNoCommonSymmetricAlgorithms syms) =- "no common recipient-supported symmetric algorithms: " ++ show syms-renderRecipientCapabilityError (RecipientCapabilityNoCommonAEADAlgorithms aeads) =- "no common recipient-supported AEAD algorithms: " ++ show aeads--data RecipientCapabilities =- RecipientCapabilities- { recipientCapabilityKeyVersion :: KeyVersion- , recipientCapabilityPublicKeyAlgorithm :: PubKeyAlgorithm- , recipientCapabilityKeyFlags :: Set.Set KeyFlag- , recipientCapabilityFeatures :: Set.Set FeatureFlag- , recipientCapabilityPreferredSymmetricAlgorithms :: [SymmetricAlgorithm]- , recipientCapabilityPreferredAEADAlgorithms :: [AEADAlgorithm]- }- deriving (Eq, Show)--data RecipientTargetRejectionReason- = RecipientTargetUnsupportedAlgorithm PubKeyAlgorithm- | RecipientTargetMissingEncryptionFlags SomePKPayload (Set.Set KeyFlag)- | RecipientTargetRevoked SomePKPayload- | RecipientTargetNotValidAtTimestamp SomePKPayload ThirtyTwoBitTimeStamp- deriving (Eq, Show)--data RecipientEncryptionTargetRejected =- RecipientEncryptionTargetRejected- { recipientEncryptionTargetRejectedKey :: SomePKPayload- , recipientEncryptionTargetRejectedCapabilities :: Maybe RecipientCapabilities- , recipientEncryptionTargetRejectedReason :: RecipientTargetRejectionReason- }- deriving (Eq, Show)--data RecipientEncryptionTargetsReport =- RecipientEncryptionTargetsReport- { recipientEncryptionTargetsAccepted :: [RecipientEncryptionTarget]- , recipientEncryptionTargetsRejected :: [RecipientEncryptionTargetRejected]- }- deriving (Eq, Show)---- | Extract encrypt-relevant recipient capabilities from effective--- self-signature subpackets.------ RFC 9580 preferred AEAD ciphersuites are currently carried through--- 'OtherSigSub' type 39 and decoded into AEAD preferences here.-recipientCapabilitiesFromSubpacketPayloads ::- SomePKPayload- -> [SigSubPacketPayload]- -> RecipientCapabilities-recipientCapabilitiesFromSubpacketPayloads recipient payloads =- foldl' step (emptyRecipientCapabilities recipient) payloads- where- preferredAEADCiphersuitesSubpacketType :: Word8- preferredAEADCiphersuitesSubpacketType = 39-- step caps payload =- case payload of- KeyFlags flags ->- caps- { recipientCapabilityKeyFlags =- recipientCapabilityKeyFlags caps `Set.union` flags- }- Features features ->- caps- { recipientCapabilityFeatures =- recipientCapabilityFeatures caps `Set.union` features- }- PreferredSymmetricAlgorithms syms ->- caps- { recipientCapabilityPreferredSymmetricAlgorithms =- recipientCapabilityPreferredSymmetricAlgorithms caps ++ syms- }- OtherSigSub subpacketType rawPayload- | subpacketType == preferredAEADCiphersuitesSubpacketType ->- caps- { recipientCapabilityPreferredAEADAlgorithms =- recipientCapabilityPreferredAEADAlgorithms caps ++- preferredAEADAlgorithmsFromCiphersuites rawPayload- }- _ -> caps-- emptyRecipientCapabilities key =- RecipientCapabilities- { recipientCapabilityKeyVersion = _keyVersion key- , recipientCapabilityPublicKeyAlgorithm = _pkalgo key- , recipientCapabilityKeyFlags = Set.empty- , recipientCapabilityFeatures = Set.empty- , recipientCapabilityPreferredSymmetricAlgorithms = []- , recipientCapabilityPreferredAEADAlgorithms = []- }-- preferredAEADAlgorithmsFromCiphersuites :: BL.ByteString -> [AEADAlgorithm]- preferredAEADAlgorithmsFromCiphersuites =- dedupePreservingOrder . parsePairs . BL.unpack- where- parsePairs (_symAlgo:aeadAlgo:rest) =- (toFVal aeadAlgo :: AEADAlgorithm) : parsePairs rest- parsePairs _ = []-- dedupePreservingOrder = foldl' addIfMissing []- addIfMissing acc x- | x `elem` acc = acc- | otherwise = acc ++ [x]--recipientCapabilitySupportsEncryption :: RecipientCapabilities -> Bool-recipientCapabilitySupportsEncryption caps =- let flags = recipientCapabilityKeyFlags caps- in Set.null flags ||- Set.member EncryptStorageKey flags ||- Set.member EncryptCommunicationsKey flags--recipientCapabilityAdvertisesSEIPDv1Support :: RecipientCapabilities -> Bool-recipientCapabilityAdvertisesSEIPDv1Support caps =- let features = recipientCapabilityFeatures caps- in Set.null features || Set.member FeatureSEIPDv1 features--recipientCapabilityAdvertisesSEIPDv2Support :: RecipientCapabilities -> Bool-recipientCapabilityAdvertisesSEIPDv2Support caps =- recipientCapabilityAdvertisesSEIPDv1Support caps &&- Set.member FeatureSEIPDv2 (recipientCapabilityFeatures caps)--recipientEncryptionTargetFromTKAtTimestamp ::- ThirtyTwoBitTimeStamp- -> TKUnknown- -> Either RecipientCapabilityError RecipientEncryptionTarget-recipientEncryptionTargetFromTKAtTimestamp timestamp tk =- recipientEncryptionTargetFromTKAtTimestampWithPolicy- RecipientTargetSelectionFirstValid- timestamp- tk--data RecipientTargetSelectionPolicy- = RecipientTargetSelectionFirstValid- | RecipientTargetSelectionPreferPrimary- | RecipientTargetSelectionPreferSubkey- | RecipientTargetSelectionPreferNewestCreationTime- deriving (Eq, Show)--recipientEncryptionTargetFromTKAtTimestampWithPolicy ::- RecipientTargetSelectionPolicy- -> ThirtyTwoBitTimeStamp- -> TKUnknown- -> Either RecipientCapabilityError RecipientEncryptionTarget-recipientEncryptionTargetFromTKAtTimestampWithPolicy policy timestamp tk =- case chooseRecipientTarget policy tk acceptedTargets of- Just target -> Right target- Nothing -> Left RecipientCapabilityNoEncryptableKeyMaterialInTK- where- acceptedTargets =- recipientEncryptionTargetsAccepted- (recipientEncryptionTargetsReportFromTKAtTimestamp timestamp tk)--recipientEncryptionTargetFromTK :: TK 'PublicTK -> Either RecipientCapabilityError RecipientEncryptionTarget-recipientEncryptionTargetFromTK tk =- recipientEncryptionTargetFromTKAtTimestampWithPolicy- RecipientTargetSelectionFirstValid- (_timestamp (keyPktPKPayload (_tkPrimaryKey tk)))- (tkToUnknown tk)--recipientEncryptionTargetFromTKWithPolicy ::- RecipientTargetSelectionPolicy- -> TK 'PublicTK- -> Either RecipientCapabilityError RecipientEncryptionTarget-recipientEncryptionTargetFromTKWithPolicy policy tk =- recipientEncryptionTargetFromTKAtTimestampWithPolicy- policy- (_timestamp (keyPktPKPayload (_tkPrimaryKey tk)))- (tkToUnknown tk)--recipientEncryptionTargetsFromTKAtTimestamp ::- ThirtyTwoBitTimeStamp- -> TKUnknown- -> [RecipientEncryptionTarget]-recipientEncryptionTargetsFromTKAtTimestamp timestamp tk =- recipientEncryptionTargetsAccepted (recipientEncryptionTargetsReportFromTKAtTimestamp timestamp tk)--recipientEncryptionTargetsReportFromTKAtTimestamp ::- ThirtyTwoBitTimeStamp- -> TKUnknown- -> RecipientEncryptionTargetsReport-recipientEncryptionTargetsReportFromTKAtTimestamp timestamp tk =- foldr classifyCandidate emptyReport (subkeyCandidates ++ [primaryCandidate])- where- emptyReport = RecipientEncryptionTargetsReport [] []- primaryCandidate = fst (_tkuKey tk)- primaryPreferencePayloads =- fromMaybe [] (effectiveKeyPreferencesAtTimestamp timestamp tk)- subkeyCandidates =- mapMaybe- (\(pkt, _) ->- case pkt of- PublicSubkeyPkt pkp -> Just pkp- SecretSubkeyPkt pkp _ -> Just pkp- _ -> Nothing)- (_tkuSubs tk)- classifyCandidate key report =- let caps =- recipientCapabilitiesFromSubpacketPayloads- key- (primaryPreferencePayloads ++ subkeyBindingCapabilityPayloads timestamp tk key)- keyStateRejection = recipientValidityRejectionReason timestamp tk key- in case keyStateRejection of- Just rejectionReason ->- report- { recipientEncryptionTargetsRejected =- RecipientEncryptionTargetRejected- { recipientEncryptionTargetRejectedKey = key- , recipientEncryptionTargetRejectedCapabilities = Just caps- , recipientEncryptionTargetRejectedReason = rejectionReason- } :- recipientEncryptionTargetsRejected report- }- Nothing ->- if not (supportsPKESKRecipientAlgorithm key)- then- report- { recipientEncryptionTargetsRejected =- RecipientEncryptionTargetRejected- { recipientEncryptionTargetRejectedKey = key- , recipientEncryptionTargetRejectedCapabilities = Just caps- , recipientEncryptionTargetRejectedReason =- RecipientTargetUnsupportedAlgorithm (_pkalgo key)- } :- recipientEncryptionTargetsRejected report- }- else- if recipientCapabilitySupportsEncryption caps- then- report- { recipientEncryptionTargetsAccepted =- recipientEncryptionTargetWithCapabilities key caps :- recipientEncryptionTargetsAccepted report- }- else- report- { recipientEncryptionTargetsRejected =- RecipientEncryptionTargetRejected- { recipientEncryptionTargetRejectedKey = key- , recipientEncryptionTargetRejectedCapabilities = Just caps- , recipientEncryptionTargetRejectedReason =- RecipientTargetMissingEncryptionFlags key (recipientCapabilityKeyFlags caps)- } :- recipientEncryptionTargetsRejected report- }--recipientEncryptionTargetsFromTK :: TK 'PublicTK -> [RecipientEncryptionTarget]-recipientEncryptionTargetsFromTK tk =- recipientEncryptionTargetsFromTKAtTimestamp- (_timestamp (keyPktPKPayload (_tkPrimaryKey tk)))- (tkToUnknown tk)--recipientEncryptionTargetsReportFromTK :: TK 'PublicTK -> RecipientEncryptionTargetsReport-recipientEncryptionTargetsReportFromTK tk =- recipientEncryptionTargetsReportFromTKAtTimestamp- (_timestamp (keyPktPKPayload (_tkPrimaryKey tk)))- (tkToUnknown tk)--subkeyBindingCapabilityPayloads ::- ThirtyTwoBitTimeStamp- -> TKUnknown- -> SomePKPayload- -> [SigSubPacketPayload]-subkeyBindingCapabilityPayloads timestamp tk recipient =- maybe [] latestEffectiveBindingPayloads matchingSubkey- where- matchingSubkey =- find- (\(pkt, _) ->- case pkt of- PublicSubkeyPkt pkp -> fingerprint pkp == fingerprint recipient- SecretSubkeyPkt pkp _ -> fingerprint pkp == fingerprint recipient- _ -> False)- (_tkuSubs tk)- latestEffectiveBindingPayloads (_, sigs) =- maybe [] signaturePayloadsFromSignature (latestEffectiveSubkeyBindingSignature timestamp sigs)--latestEffectiveSubkeyBindingSignature ::- ThirtyTwoBitTimeStamp- -> [SignaturePayload]- -> Maybe SignaturePayload-latestEffectiveSubkeyBindingSignature timestamp sigs =- case filter (isEffectiveSubkeyBindingSignature timestamp) sigs of- [] -> Nothing- candidates -> Just (maximumBy (comparing signatureCreationTimestamp) candidates)--isEffectiveSubkeyBindingSignature ::- ThirtyTwoBitTimeStamp- -> SignaturePayload- -> Bool-isEffectiveSubkeyBindingSignature timestamp sig =- isSubkeyBindingSig sig &&- maybe False- (\created ->- let tsValue = toInteger (unThirtyTwoBitTimeStamp timestamp)- createdValue = toInteger (unThirtyTwoBitTimeStamp created)- in createdValue <= tsValue &&- maybe True- (\duration ->- if unThirtyTwoBitDuration duration == 0- then True- else tsValue < createdValue + toInteger (unThirtyTwoBitDuration duration))- (signatureExpirationDuration sig))- (sigCT sig)--signatureCreationTimestamp :: SignaturePayload -> ThirtyTwoBitTimeStamp-signatureCreationTimestamp sig =- fromMaybe (ThirtyTwoBitTimeStamp 0) (sigCT sig)--signatureExpirationDuration :: SignaturePayload -> Maybe ThirtyTwoBitDuration-signatureExpirationDuration sig =- case signatureHashedSubpacketsKnown sig of- Just hashed ->- foldr- (\subpacket acc ->- case subpacket of- SigSubPacket _ (SigExpirationTime duration) -> Just duration- _ -> acc)- Nothing- hashed- Nothing -> Nothing--signaturePayloadsFromSignature :: SignaturePayload -> [SigSubPacketPayload]-signaturePayloadsFromSignature sig =- case signatureHashedSubpacketsKnown sig of- Just hashed -> map (\(SigSubPacket _ payload) -> payload) hashed- Nothing -> []--recipientValidityRejectionReason ::- ThirtyTwoBitTimeStamp- -> TKUnknown- -> SomePKPayload- -> Maybe RecipientTargetRejectionReason-recipientValidityRejectionReason timestamp tk key- | fingerprint key == fingerprint (fst (_tkuKey tk)) =- if keyStateValid (keyStateAt (timestampToUTC timestamp) tk)- then Nothing- else Just (RecipientTargetNotValidAtTimestamp key timestamp)- | otherwise =- case findMatchingSubkeySignatures tk key of- Nothing -> Nothing- Just sigs- | subkeyRevokedAtTimestamp timestamp sigs ->- Just (RecipientTargetRevoked key)- | isPKTimeValidWithSelfSignatures (timestampToUTC timestamp) key sigs ->- Nothing- | otherwise ->- Just (RecipientTargetNotValidAtTimestamp key timestamp)--findMatchingSubkeySignatures :: TKUnknown -> SomePKPayload -> Maybe [SignaturePayload]-findMatchingSubkeySignatures tk recipient =- snd <$>- find- (\(pkt, _) ->- case pkt of- PublicSubkeyPkt pkp -> fingerprint pkp == fingerprint recipient- SecretSubkeyPkt pkp _ -> fingerprint pkp == fingerprint recipient- _ -> False)- (_tkuSubs tk)--subkeyRevokedAtTimestamp :: ThirtyTwoBitTimeStamp -> [SignaturePayload] -> Bool-subkeyRevokedAtTimestamp timestamp =- any (\sig -> isSubkeyRevocation sig && signatureEffectiveAt (timestampToUTC timestamp) sig)--timestampToUTC :: ThirtyTwoBitTimeStamp -> UTCTime-timestampToUTC =- posixSecondsToUTCTime . realToFrac . unThirtyTwoBitTimeStamp--supportsPKESKRecipientAlgorithm :: SomePKPayload -> Bool-supportsPKESKRecipientAlgorithm recipient =- case _pkalgo recipient of- RSA -> True- DeprecatedRSAEncryptOnly -> True- ECDH -> True- X25519 -> True- X448 -> True- _ -> False--chooseRecipientTarget ::- RecipientTargetSelectionPolicy- -> TKUnknown- -> [RecipientEncryptionTarget]- -> Maybe RecipientEncryptionTarget-chooseRecipientTarget policy tk targets =- case policy of- RecipientTargetSelectionFirstValid -> listToMaybe targets- RecipientTargetSelectionPreferPrimary ->- listToMaybe (filter (isPrimaryTarget tk) targets) <|> listToMaybe targets- RecipientTargetSelectionPreferSubkey ->- listToMaybe (filter (not . isPrimaryTarget tk) targets) <|> listToMaybe targets- RecipientTargetSelectionPreferNewestCreationTime ->- case targets of- [] -> Nothing- (target:rest) ->- Just- (foldl'- (\best candidate ->- if _timestamp (recipientEncryptionTargetKey candidate) >- _timestamp (recipientEncryptionTargetKey best)- then candidate- else best)- target- rest)- where- isPrimaryTarget currentTK target =- fingerprint (recipientEncryptionTargetKey target) ==- fingerprint (fst (_tkuKey currentTK))---- | Session-key bundle for PKESK/SKESK packet construction.-newtype PKESKV3SessionMaterial =- PKESKV3SessionMaterial- { unPKESKV3SessionMaterial :: B.ByteString- }- deriving (Eq, Show)--newtype PKESKV6RawSessionMaterial =- PKESKV6RawSessionMaterial- { unPKESKV6RawSessionMaterial :: B.ByteString- }- deriving (Eq, Show)--data PKESKSessionMaterial =- PKESKSessionMaterial- { pkeskSessionAlgorithm :: SymmetricAlgorithm- , pkeskSessionKey :: SessionKey- , pkeskEncodedSessionMaterial :: B.ByteString- }- deriving (Eq, Show)--mkPKESKSessionMaterial ::- SymmetricAlgorithm- -> SessionKey- -> Either PKESKEncryptError PKESKSessionMaterial-mkPKESKSessionMaterial symalgo sessionKey = do- v3Material <- mkPKESKV3SessionMaterial symalgo sessionKey- _v6Material <- mkPKESKV6RawSessionMaterial symalgo sessionKey- pure- PKESKSessionMaterial- { pkeskSessionAlgorithm = symalgo- , pkeskSessionKey = sessionKey- , pkeskEncodedSessionMaterial = unPKESKV3SessionMaterial v3Material- }--mkPKESKV3SessionMaterial ::- SymmetricAlgorithm- -> SessionKey- -> Either PKESKEncryptError PKESKV3SessionMaterial-mkPKESKV3SessionMaterial symalgo sessionKey = do- keyBytes <- validatedSessionKeyBytes symalgo sessionKey- pure $- PKESKV3SessionMaterial- (B.singleton (fromFVal symalgo) <> keyBytes <> checksum16Bytes keyBytes)--mkPKESKV6RawSessionMaterial ::- SymmetricAlgorithm- -> SessionKey- -> Either PKESKEncryptError PKESKV6RawSessionMaterial-mkPKESKV6RawSessionMaterial symalgo sessionKey =- PKESKV6RawSessionMaterial <$> validatedSessionKeyBytes symalgo sessionKey--pkeskV3SessionMaterial :: PKESKSessionMaterial -> PKESKV3SessionMaterial-pkeskV3SessionMaterial =- PKESKV3SessionMaterial . pkeskEncodedSessionMaterial--pkeskV6RawSessionMaterial :: PKESKSessionMaterial -> PKESKV6RawSessionMaterial-pkeskV6RawSessionMaterial =- PKESKV6RawSessionMaterial . unSessionKey . pkeskSessionKey--validatedSessionKeyBytes ::- SymmetricAlgorithm- -> SessionKey- -> Either PKESKEncryptError B.ByteString-validatedSessionKeyBytes symalgo (SessionKey sessionKey) = do- keyLen <-- first- (UnsupportedSessionKeyAlgorithm symalgo . renderCipherError)- (keySize symalgo)- let actualLen = B.length sessionKey- if actualLen /= keyLen- then Left (InvalidSessionKeyLength symalgo keyLen actualLen)- else Right sessionKey--data RecipientPKESKVersionStrategy- = RecipientPreferV6- | RecipientForceV3Interop- deriving (Eq, Show)--data RecipientPKESKVersionStrategyW (strategy :: RecipientPKESKVersionStrategy) where- RecipientPreferV6W :: RecipientPKESKVersionStrategyW 'RecipientPreferV6- RecipientForceV3InteropW :: RecipientPKESKVersionStrategyW 'RecipientForceV3Interop--data SomeRecipientPKESKVersionStrategyW where- SomeRecipientPKESKVersionStrategyW ::- RecipientPKESKVersionStrategyW strategy- -> SomeRecipientPKESKVersionStrategyW--type RecipientPKESKVersionSelector =- SomePKPayload -> Either PKESKEncryptError RecipientPKESKVersionStrategy--type RecipientPKESKVersionSelectorTyped =- SomePKPayload -> Either PKESKEncryptError SomeRecipientPKESKVersionStrategyW--data EncryptCompatibilityProfile- = EncryptStrictDefault- | EncryptInteropLegacy- deriving (Eq, Show)--data EncryptCompatibilityProfileW (profile :: EncryptCompatibilityProfile) where- EncryptStrictDefaultW :: EncryptCompatibilityProfileW 'EncryptStrictDefault- EncryptInteropLegacyW :: EncryptCompatibilityProfileW 'EncryptInteropLegacy--data SomeEncryptCompatibilityProfileW where- SomeEncryptCompatibilityProfileW ::- EncryptCompatibilityProfileW profile- -> SomeEncryptCompatibilityProfileW--data SEIPDVersion- = SEIPDv1- | SEIPDv2- deriving (Eq, Show)--type family PayloadVersionForProfile (profile :: EncryptCompatibilityProfile) :: SEIPDVersion where- PayloadVersionForProfile 'EncryptStrictDefault = 'SEIPDv2- PayloadVersionForProfile 'EncryptInteropLegacy = 'SEIPDv1--type family ProfileForPayloadVersion (version :: SEIPDVersion) :: EncryptCompatibilityProfile where- ProfileForPayloadVersion 'SEIPDv1 = 'EncryptInteropLegacy- ProfileForPayloadVersion 'SEIPDv2 = 'EncryptStrictDefault--data RecipientEncryptionTarget =- RecipientEncryptionTarget- { -- | Recipient key packet selected for PKESK wrapping.- recipientEncryptionTargetKey :: SomePKPayload- -- | Optional explicit PKESK version strategy hint.- -- When absent, profile defaults and auto-detection apply.- , recipientEncryptionTargetStrategy :: Maybe RecipientPKESKVersionStrategy- -- | Optional recipient capability hints used by negotiation-enabled- -- encryption to choose common symmetric/AEAD algorithms.- , recipientEncryptionTargetCapabilities :: Maybe RecipientCapabilities- }- deriving (Eq, Show)--recipientEncryptionTarget :: SomePKPayload -> RecipientEncryptionTarget-recipientEncryptionTarget recipient =- RecipientEncryptionTarget recipient Nothing Nothing--recipientEncryptionTargetWithStrategy :: SomePKPayload -> RecipientPKESKVersionStrategy -> RecipientEncryptionTarget-recipientEncryptionTargetWithStrategy recipient strategy =- RecipientEncryptionTarget recipient (Just strategy) Nothing--recipientEncryptionTargetWithCapabilities ::- SomePKPayload- -> RecipientCapabilities- -> RecipientEncryptionTarget-recipientEncryptionTargetWithCapabilities recipient capabilities =- RecipientEncryptionTarget recipient Nothing (Just capabilities)--recipientEncryptionTargetWithStrategyTyped :: SomePKPayload- -> RecipientPKESKVersionStrategyW strategy- -> RecipientEncryptionTarget-recipientEncryptionTargetWithStrategyTyped recipient strategyW =- recipientEncryptionTargetWithStrategy recipient (demoteRecipientStrategy strategyW)--recipientVersionStrategyForProfile ::- EncryptCompatibilityProfile- -> RecipientEncryptionTarget- -> Either PKESKEncryptError RecipientPKESKVersionStrategy-recipientVersionStrategyForProfile profile target =- case promoteEncryptCompatibilityProfile profile of- SomeEncryptCompatibilityProfileW profileW ->- demoteSomeRecipientStrategy <$>- recipientVersionStrategyForProfileTyped profileW target--recipientVersionStrategyForProfileTyped ::- EncryptCompatibilityProfileW profile- -> RecipientEncryptionTarget- -> Either PKESKEncryptError SomeRecipientPKESKVersionStrategyW-recipientVersionStrategyForProfileTyped profile target =- Right $- case recipientEncryptionTargetStrategy target of- Just strategy ->- promoteRecipientStrategy strategy- Nothing ->- case profile of- EncryptStrictDefaultW ->- autoDetectRecipientVersionStrategy- (recipientEncryptionTargetKey target)- EncryptInteropLegacyW ->- SomeRecipientPKESKVersionStrategyW RecipientForceV3InteropW--profileForPayloadVersionW ::- RecipientEncryptRequestOverrides version- -> EncryptCompatibilityProfileW (ProfileForPayloadVersion version)-profileForPayloadVersionW overrides =- case overrides of- RecipientEncryptRequestSEIPDv2Overrides {} -> EncryptStrictDefaultW- RecipientEncryptRequestSEIPDv1Overrides {} -> EncryptInteropLegacyW--autoDetectRecipientVersionStrategy :: SomePKPayload- -> SomeRecipientPKESKVersionStrategyW-autoDetectRecipientVersionStrategy recipient- | _keyVersion recipient == V6 =- SomeRecipientPKESKVersionStrategyW RecipientPreferV6W- | _pkalgo recipient `elem` [X25519, X448] =- SomeRecipientPKESKVersionStrategyW RecipientPreferV6W- | otherwise =- SomeRecipientPKESKVersionStrategyW RecipientForceV3InteropW--data RecipientPayloadShape =- RecipientPayloadShape- { recipientPayloadDataType :: DataType- , recipientPayloadFileName :: FileName- , recipientPayloadTimestamp :: ThirtyTwoBitTimeStamp- , recipientPayloadUseOnePassSignatures :: Bool- , recipientPayloadSignatures :: [SignaturePayload]- }- deriving (Eq, Show)--data PassphraseSKESKVersionPolicy- = PassphraseSKESKPreferV6- | PassphraseSKESKForceV4Interop- deriving (Eq, Show)--data PassphraseEncryptRequest =- PassphraseEncryptRequest- { passphraseEncryptVersionPolicy :: PassphraseSKESKVersionPolicy- , passphraseEncryptSymmetricAlgorithm :: SymmetricAlgorithm- , passphraseEncryptS2K :: S2K- , passphraseEncryptPassphrase :: BL.ByteString- , passphraseEncryptPayload :: B.ByteString- , passphraseEncryptSEIPDv1IVOverride :: Maybe IV- , passphraseEncryptSEIPDv2AEADOverride :: Maybe AEADAlgorithm- , passphraseEncryptSEIPDv2ChunkSizeOverride :: Maybe Word8- , passphraseEncryptSEIPDv2SaltOverride :: Maybe Salt- }- deriving (Eq, Show)--encryptPassphraseWithPolicy ::- MonadRandom m- => PassphraseEncryptRequest- -> m (Either String [Pkt])-encryptPassphraseWithPolicy request =- case passphraseEncryptVersionPolicy request of- PassphraseSKESKForceV4Interop ->- encryptSEIPDv1WithSKESK- (passphraseEncryptSymmetricAlgorithm request)- (passphraseEncryptS2K request)- (passphraseEncryptSEIPDv1IVOverride request)- (passphraseEncryptPassphrase request)- (passphraseEncryptPayload request)- PassphraseSKESKPreferV6 -> do- let messagePolicy = policyMessageEncryption (policyForRFC RFC9580)- aead =- fromMaybe- (messageDefaultAEADAlgorithm messagePolicy)- (passphraseEncryptSEIPDv2AEADOverride request)- chunkSize =- fromMaybe- (messageDefaultChunkSize messagePolicy)- (passphraseEncryptSEIPDv2ChunkSizeOverride request)- salt <-- maybe- (Salt <$> getRandomBytes (messageSEIPDv2SaltOctets messagePolicy))- pure- (passphraseEncryptSEIPDv2SaltOverride request)- pure $- encryptSEIPDv2WithSKESK- (passphraseEncryptSymmetricAlgorithm request)- aead- chunkSize- salt- (passphraseEncryptS2K request)- (passphraseEncryptPassphrase request)- (passphraseEncryptPayload request)--defaultRecipientPayloadShape :: RecipientPayloadShape-defaultRecipientPayloadShape =- RecipientPayloadShape- { recipientPayloadDataType = BinaryData- , recipientPayloadFileName = BL.empty- , recipientPayloadTimestamp = 0- , recipientPayloadUseOnePassSignatures = False- , recipientPayloadSignatures = []- }--data RecipientEncryptResult =- RecipientEncryptResult- { recipientEncryptPackets :: [Pkt]- , recipientEncryptSessionMaterial :: PKESKSessionMaterial- }- deriving (Eq, Show)--data RecipientEncryptRequestOverrides (v :: SEIPDVersion) where- RecipientEncryptRequestSEIPDv1Overrides ::- { recipientEncryptRequestIVOverride :: Maybe IV- } -> RecipientEncryptRequestOverrides 'SEIPDv1- -- | For SEIPDv2 requests:- -- - when AEAD override is 'Nothing', encrypt-side capability negotiation- -- selects a common recipient-supported AEAD algorithm (if enabled).- -- - when AEAD override is 'Just', the explicit AEAD wins.- RecipientEncryptRequestSEIPDv2Overrides ::- { recipientEncryptRequestAEADOverride :: Maybe AEADAlgorithm- , recipientEncryptRequestChunkSizeOverride :: Maybe Word8- , recipientEncryptRequestSaltOverride :: Maybe Salt- } -> RecipientEncryptRequestOverrides 'SEIPDv2--data RecipientEncryptRequest (v :: SEIPDVersion) =- RecipientEncryptRequest- { -- | Recipient encryption targets. At least one target is required.- recipientEncryptRequestTargets :: [RecipientEncryptionTarget]- , recipientEncryptRequestPayloadShape :: RecipientPayloadShape- , recipientEncryptRequestPayload :: B.ByteString- -- | Explicit symmetric algorithm override. When 'Nothing', the selected- -- mode (negotiated or legacy) determines algorithm selection.- , recipientEncryptRequestSymmetricOverride :: Maybe SymmetricAlgorithm- , recipientEncryptRequestOverrides :: RecipientEncryptRequestOverrides v- }--deriving instance Eq (RecipientEncryptRequestOverrides v)-deriving instance Show (RecipientEncryptRequestOverrides v)-deriving instance Eq (RecipientEncryptRequest v)-deriving instance Show (RecipientEncryptRequest v)---- | Encode the RFC 9580 PKESK/SKESK session-key material:--- one-octet algorithm ID, raw session key, then 16-bit checksum.-encodeOpenPGPSessionMaterial ::- SymmetricAlgorithm -> SessionKey -> Either PKESKEncryptError B.ByteString-encodeOpenPGPSessionMaterial symalgo sessionKey =- unPKESKV3SessionMaterial <$> mkPKESKV3SessionMaterial symalgo sessionKey---- | Generate a fresh session key and return both raw and encoded forms.-generateSessionKeyMaterial ::- MonadRandom m- => SymmetricAlgorithm- -> m (Either PKESKEncryptError PKESKSessionMaterial)-generateSessionKeyMaterial symalgo =- case keySize symalgo of- Left err -> pure (Left (UnsupportedSessionKeyAlgorithm symalgo (renderCipherError err)))- Right keyLen -> do- sessionKeyBytes <- getRandomBytes keyLen- let sessionKey = SessionKey sessionKeyBytes- pure (mkPKESKSessionMaterial symalgo sessionKey)--canonicalizePKESKRecipientId ::- PKESKPayload -> Either PKESKEncryptError PKESKPayload-canonicalizePKESKRecipientId payload =- case payload of- PKESKPayloadV6Packet payloadV6 ->- PKESKPayloadV6Packet <$> canonicalizePKESKRecipientIdV6 payloadV6- _ -> Right payload--canonicalizePKESKRecipientIdV6 ::- PKESKPayloadV6 -> Either PKESKEncryptError PKESKPayloadV6-canonicalizePKESKRecipientIdV6 (PKESKPayloadV6 rid pka esk) =- (\normalizedRid -> PKESKPayloadV6 normalizedRid pka esk) <$>- canonicalizeRecipientKeyIdentifier rid--canonicalizeRecipientKeyIdentifier ::- BL.ByteString -> Either PKESKEncryptError BL.ByteString-canonicalizeRecipientKeyIdentifier rid- | BL.length rid == 20 || BL.length rid == 32 = Right rid- | BL.length rid == 21 && BL.head rid == 0x04 = Right (BL.tail rid)- | BL.length rid == 33 && BL.head rid == 0x06 = Right (BL.tail rid)- | otherwise =- Left- (InvalidRecipientIdentifier- ("unsupported PKESK recipient identifier length/prefix: " ++- show (BL.length rid)))--canonicalizePKESKPacketRecipientIds ::- [Pkt] -> Either PKESKEncryptError [Pkt]-canonicalizePKESKPacketRecipientIds =- mapM- (\pkt ->- case pkt of- PKESKPkt payload -> fmap PKESKPkt (canonicalizePKESKRecipientId payload)- _ -> Right pkt)---- | Build a v6 PKESK payload for one recipient key according to the selected version policy.-buildPKESKPayloadForRecipient ::- MonadRandom m- => PKESKVersionPolicy- -> SomePKPayload- -> PKESKSessionMaterial- -> m (Either PKESKEncryptError PKESKPayload)-buildPKESKPayloadForRecipient policy recipient material =- case policy of- ForceV3Interop ->- buildPKESKv3PayloadForRecipient recipient (pkeskV3SessionMaterial material)- PreferV6 ->- case _pkalgo recipient of- RSA -> fmap (fmap PKESKPayloadV6Packet) (buildRsaPKESKv6 recipient material)- ECDH -> fmap (fmap PKESKPayloadV6Packet) (buildECDHPKESKv6 recipient material)- X25519 ->- fmap- (fmap PKESKPayloadV6Packet)- (buildX25519PKESKv6 recipient (pkeskV6RawSessionMaterial material))- X448 ->- fmap- (fmap PKESKPayloadV6Packet)- (buildX448PKESKv6 recipient (pkeskV6RawSessionMaterial material))- pka -> pure (Left (UnsupportedRecipientAlgorithm pka))----- | Build a PKESK packet for one recipient key according to the selected version policy.-buildPKESKPktForRecipient ::- MonadRandom m- => PKESKVersionPolicy- -> SomePKPayload- -> PKESKSessionMaterial- -> m (Either PKESKEncryptError Pkt)-buildPKESKPktForRecipient policy recipient material =- fmap- (fmap PKESKPkt)- (buildPKESKPayloadForRecipient policy recipient material)----- | Build a legacy PKESKv3 payload for v4/v3 RSA recipient interop.-buildPKESKv3PayloadForRecipient ::- MonadRandom m- => SomePKPayload- -> PKESKV3SessionMaterial- -> m (Either PKESKEncryptError PKESKPayload)-buildPKESKv3PayloadForRecipient recipient material =- fmap (fmap PKESKPayloadV3Packet) (buildPKESKv3PayloadForRecipientTyped recipient material)--buildPKESKv3PayloadForRecipientTyped ::- MonadRandom m- => SomePKPayload- -> PKESKV3SessionMaterial- -> m (Either PKESKEncryptError PKESKPayloadV3)-buildPKESKv3PayloadForRecipientTyped recipient material =- case _pkalgo recipient of- RSA -> buildRsaPKESKv3 recipient material- DeprecatedRSAEncryptOnly -> buildRsaPKESKv3 recipient material- ECDH -> buildECDHPKESKv3 recipient material- pka -> pure (Left (UnsupportedRecipientAlgorithm pka))---- | Build a legacy PKESKv3 packet for v4/v3 RSA recipient interop.-buildPKESKv3PktForRecipient ::- MonadRandom m- => SomePKPayload- -> PKESKV3SessionMaterial- -> m (Either PKESKEncryptError Pkt)-buildPKESKv3PktForRecipient recipient material =- fmap (fmap PKESKPkt) (buildPKESKv3PayloadForRecipient recipient material)---- | Build PKESK packets for all recipients with a single shared session key.--buildPKESKPktsForRecipientTargetsWithSelector ::- MonadRandom m- => (RecipientEncryptionTarget -> Either PKESKEncryptError RecipientPKESKVersionStrategy)- -> [RecipientEncryptionTarget]- -> PKESKSessionMaterial- -> m (Either PKESKEncryptError [Pkt])-buildPKESKPktsForRecipientTargetsWithSelector selector targets material- = buildPKESKPktsForRecipientTargetsWithSelectorTyped- (\target ->- promoteRecipientStrategy <$> selector target)- targets- material--buildPKESKPktsForRecipientTargetsWithSelectorTyped ::- MonadRandom m- => (RecipientEncryptionTarget -> Either PKESKEncryptError SomeRecipientPKESKVersionStrategyW)- -> [RecipientEncryptionTarget]- -> PKESKSessionMaterial- -> m (Either PKESKEncryptError [Pkt])-buildPKESKPktsForRecipientTargetsWithSelectorTyped selector targets material- | null targets = pure (Left NoRecipientsProvided)- | otherwise =- case preparePKESKVersionedMaterial material of- Left err -> pure (Left err)- Right (v3Material, v6RawMaterial) -> do- pkeskResults <-- mapM- (\target ->- case selector target of- Left err -> pure (Left err)- Right (SomeRecipientPKESKVersionStrategyW RecipientPreferV6W) ->- buildPKESKPktForRecipientWithPreparedPayload- RecipientPreferV6W- (recipientEncryptionTargetKey target)- (RecipientPreferV6Payload material v6RawMaterial)- Right (SomeRecipientPKESKVersionStrategyW RecipientForceV3InteropW) ->- buildPKESKPktForRecipientWithPreparedPayload- RecipientForceV3InteropW- (recipientEncryptionTargetKey target)- (RecipientForceV3Payload v3Material))- targets- pure (sequence pkeskResults >>= canonicalizePKESKPacketRecipientIds)--preparePKESKVersionedMaterial ::- PKESKSessionMaterial- -> Either PKESKEncryptError (PKESKV3SessionMaterial, PKESKV6RawSessionMaterial)-preparePKESKVersionedMaterial material = do- v3Material <-- mkPKESKV3SessionMaterial- (pkeskSessionAlgorithm material)- (pkeskSessionKey material)- v6RawMaterial <-- mkPKESKV6RawSessionMaterial- (pkeskSessionAlgorithm material)- (pkeskSessionKey material)- pure (v3Material, v6RawMaterial)--data RecipientPKESKRequestPayload (strategy :: RecipientPKESKVersionStrategy) where- RecipientForceV3Payload ::- PKESKV3SessionMaterial- -> RecipientPKESKRequestPayload 'RecipientForceV3Interop- RecipientPreferV6Payload ::- PKESKSessionMaterial- -> PKESKV6RawSessionMaterial- -> RecipientPKESKRequestPayload 'RecipientPreferV6--buildPKESKPktForRecipientWithPreparedPayload ::- MonadRandom m- => RecipientPKESKVersionStrategyW strategy- -> SomePKPayload- -> RecipientPKESKRequestPayload strategy- -> m (Either PKESKEncryptError Pkt)-buildPKESKPktForRecipientWithPreparedPayload strategy recipient payload =- fmap fmapPKESKPkt payloadResult- where- fmapPKESKPkt = fmap PKESKPkt- payloadResult =- case (strategy, payload) of- (RecipientForceV3InteropW, RecipientForceV3Payload v3Material) ->- buildPKESKv3PayloadForRecipient recipient v3Material- (RecipientPreferV6W, RecipientPreferV6Payload material v6RawMaterial) ->- case _pkalgo recipient of- RSA ->- fmap (fmap PKESKPayloadV6Packet) (buildRsaPKESKv6 recipient material)- ECDH ->- fmap (fmap PKESKPayloadV6Packet) (buildECDHPKESKv6 recipient material)- X25519 ->- fmap- (fmap PKESKPayloadV6Packet)- (buildX25519PKESKv6 recipient v6RawMaterial)- X448 ->- fmap- (fmap PKESKPayloadV6Packet)- (buildX448PKESKv6 recipient v6RawMaterial)- pka ->- pure (Left (UnsupportedRecipientAlgorithm pka))---- | Encrypt for recipient targets with capability negotiation enabled.------ By default this negotiates a common symmetric and (for SEIPDv2) AEAD--- algorithm from recipient capabilities when available. Explicit request--- overrides still take precedence.-encryptForRecipients ::- MonadRandom m- => RecipientEncryptRequest v- -> m (Either PKESKEncryptError RecipientEncryptResult)-encryptForRecipients =- encryptForRecipientsWithCapabilityNegotiation RecipientCapabilityNegotiationOn---- | Encrypt for recipient targets without recipient capability negotiation.------ This preserves legacy behavior by using policy defaults unless request--- overrides are provided.-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--- intersection of recipient preferences constrained by the active policy.--- When off, policy defaults are used.-encryptForRecipientsWithCapabilityNegotiation ::- MonadRandom m- => RecipientCapabilityNegotiationMode- -> RecipientEncryptRequest v- -> m (Either PKESKEncryptError RecipientEncryptResult)-encryptForRecipientsWithCapabilityNegotiation negotiationMode request- | null targets = pure (Left NoRecipientsProvided)- | otherwise =- case selectSymmetricAlgorithm negotiationMode request messagePolicy targets of- Left err -> pure (Left err)- Right symalgo -> do- sessionMaterialResult <- generateSessionKeyMaterial symalgo- case sessionMaterialResult of- Left err -> pure (Left err)- Right sessionMaterial -> do- pkeskResult <-- buildPKESKPktsForRecipientTargetsWithSelectorTyped- (recipientVersionStrategyForProfileTyped profileW)- targets- sessionMaterial- case pkeskResult of- Left err -> pure (Left err)- Right pkeskPkts -> do- payloadResult <- case recipientEncryptRequestOverrides request of- RecipientEncryptRequestSEIPDv2Overrides- { recipientEncryptRequestAEADOverride = aeadOverride- , recipientEncryptRequestChunkSizeOverride = chunkSizeOverride- , recipientEncryptRequestSaltOverride = saltOverride- } ->- case recipientsMissingSEIPDv2Support targets of- [] -> do- case selectAEADAlgorithm negotiationMode messagePolicy targets aeadOverride of- Left err -> pure (Left err)- Right aead -> do- salt <- maybe (Salt <$> getRandomBytes 32) pure saltOverride- let chunkSize =- maybe- (messageDefaultChunkSize messagePolicy)- id- chunkSizeOverride- pure $- buildEncryptedPacketSequenceWithShape- symalgo- aead- chunkSize- (recipientEncryptRequestPayloadShape request)- salt- (pkeskSessionKey sessionMaterial)- pkeskPkts- (recipientEncryptRequestPayload request)- _missingSEIPDv2 ->- case recipientsMissingSEIPDv1Support targets of- [] ->- buildSEIPDv1PayloadWithIV- symalgo- sessionMaterial- pkeskPkts- Nothing- missingSEIPDv1 ->- pure- (Left- (RecipientCapabilitySelectionFailure- (RecipientCapabilityMissingSEIPDv1Support missingSEIPDv1)))- RecipientEncryptRequestSEIPDv1Overrides- { recipientEncryptRequestIVOverride = ivOverride } ->- case recipientsMissingSEIPDv1Support targets of- [] ->- buildSEIPDv1PayloadWithIV- symalgo- sessionMaterial- pkeskPkts- ivOverride- missingSEIPDv1 ->- pure- (Left- (RecipientCapabilitySelectionFailure- (RecipientCapabilityMissingSEIPDv1Support missingSEIPDv1)))- pure $- fmap- (\pkts ->- RecipientEncryptResult- { recipientEncryptPackets = pkts- , recipientEncryptSessionMaterial = sessionMaterial- })- payloadResult- where- targets = recipientEncryptRequestTargets request- profileW =- profileForPayloadVersionW (recipientEncryptRequestOverrides request)- messagePolicy =- case profileW of- EncryptStrictDefaultW ->- policyMessageEncryption (policyForRFC RFC9580)- EncryptInteropLegacyW ->- policyMessageEncryption (policyForRFC RFC4880)-- buildSEIPDv1PayloadWithIV ::- MonadRandom m- => SymmetricAlgorithm- -> PKESKSessionMaterial- -> [Pkt]- -> Maybe IV- -> m (Either PKESKEncryptError [Pkt])- buildSEIPDv1PayloadWithIV symalgo sessionMaterial pkeskPkts ivOverride = do- ivResult <-- case ivOverride of- Just iv -> pure (Right iv)- Nothing ->- let keyBytes = unSessionKey (pkeskSessionKey sessionMaterial)- in case withSymmetricCipher symalgo keyBytes (\c -> Right (blockSize c)) of- Left err -> pure (Left (PayloadBuildFailure (renderCipherError err)))- Right n -> fmap (Right . IV) (getRandomBytes n)- case ivResult of- Left err -> pure (Left err)- Right iv ->- pure $- buildEncryptedPacketSequenceWithShapeSEIPDv1- symalgo- iv- (recipientEncryptRequestPayloadShape request)- (pkeskSessionKey sessionMaterial)- pkeskPkts- (recipientEncryptRequestPayload request)-- recipientsMissingSEIPDv1Support :: [RecipientEncryptionTarget] -> [SomePKPayload]- recipientsMissingSEIPDv1Support =- map recipientEncryptionTargetKey .- filter (not . targetAdvertisesSEIPDv1Support)-- recipientsMissingSEIPDv2Support :: [RecipientEncryptionTarget] -> [SomePKPayload]- recipientsMissingSEIPDv2Support =- map recipientEncryptionTargetKey .- filter (not . targetAdvertisesSEIPDv2Support)-- targetAdvertisesSEIPDv1Support :: RecipientEncryptionTarget -> Bool- targetAdvertisesSEIPDv1Support target =- case recipientEncryptionTargetCapabilities target of- Nothing -> True- Just caps -> recipientCapabilityAdvertisesSEIPDv1Support caps-- targetAdvertisesSEIPDv2Support :: RecipientEncryptionTarget -> Bool- targetAdvertisesSEIPDv2Support target =- case recipientEncryptionTargetCapabilities target of- Nothing -> True- Just caps -> recipientCapabilityAdvertisesSEIPDv2Support caps--selectSymmetricAlgorithm ::- RecipientCapabilityNegotiationMode- -> RecipientEncryptRequest v- -> MessageEncryptionPolicy- -> [RecipientEncryptionTarget]- -> Either PKESKEncryptError SymmetricAlgorithm-selectSymmetricAlgorithm negotiationMode request messagePolicy targets =- case recipientEncryptRequestSymmetricOverride request of- Just override -> Right override- Nothing ->- case negotiationMode of- RecipientCapabilityNegotiationOff ->- Right (messageDefaultSymmetricAlgorithm messagePolicy)- RecipientCapabilityNegotiationOn ->- negotiateSymmetricAlgorithm messagePolicy targets--selectAEADAlgorithm ::- RecipientCapabilityNegotiationMode- -> MessageEncryptionPolicy- -> [RecipientEncryptionTarget]- -> Maybe AEADAlgorithm- -> Either PKESKEncryptError AEADAlgorithm-selectAEADAlgorithm negotiationMode messagePolicy targets override =- case override of- Just explicit -> Right explicit- Nothing ->- case negotiationMode of- RecipientCapabilityNegotiationOff ->- Right (messageDefaultAEADAlgorithm messagePolicy)- RecipientCapabilityNegotiationOn ->- negotiateAEADAlgorithm messagePolicy targets--negotiateSymmetricAlgorithm ::- MessageEncryptionPolicy- -> [RecipientEncryptionTarget]- -> Either PKESKEncryptError SymmetricAlgorithm-negotiateSymmetricAlgorithm messagePolicy targets =- chooseCommonAlgorithm- policyOrder- recipientChoices- (RecipientCapabilityNoCommonSymmetricAlgorithms (concat recipientChoices))- where- policyOrder =- case messageSEIPDv2SymmetricAlgorithms messagePolicy of- [] -> [messageDefaultSymmetricAlgorithm messagePolicy]- syms -> syms- recipientChoices = map choicesForTarget targets- choicesForTarget target =- case recipientEncryptionTargetCapabilities target of- Just caps ->- let preferred = recipientCapabilityPreferredSymmetricAlgorithms caps- allowed = [alg | alg <- policyOrder, alg `elem` preferred]- in if null allowed- then policyOrder- else allowed- Nothing -> policyOrder--negotiateAEADAlgorithm ::- MessageEncryptionPolicy- -> [RecipientEncryptionTarget]- -> Either PKESKEncryptError AEADAlgorithm-negotiateAEADAlgorithm messagePolicy targets =- chooseCommonAlgorithm- policyOrder- recipientChoices- (RecipientCapabilityNoCommonAEADAlgorithms (concat recipientChoices))- where- policyOrder = foldl' addIfMissing [] (messageDefaultAEADAlgorithm messagePolicy : [OCB, EAX, GCM])- recipientChoices = map choicesForTarget targets- choicesForTarget target =- case recipientEncryptionTargetCapabilities target of- Just caps ->- let preferred = recipientCapabilityPreferredAEADAlgorithms caps- allowed = [alg | alg <- policyOrder, alg `elem` preferred]- in if null allowed- then policyOrder- else allowed- Nothing -> policyOrder- addIfMissing acc x- | x `elem` acc = acc- | otherwise = acc ++ [x]--chooseCommonAlgorithm ::- Eq a- => [a]- -> [[a]]- -> RecipientCapabilityError- -> Either PKESKEncryptError a-chooseCommonAlgorithm policyOrder recipientChoices err =- case recipientChoices of- [] -> Left (RecipientCapabilitySelectionFailure err)- (firstChoices:restChoices) ->- let common = foldl' intersectOrdered firstChoices restChoices- orderedCommon = [alg | alg <- policyOrder, alg `elem` common]- in case orderedCommon of- (selected:_) -> Right selected- [] -> Left (RecipientCapabilitySelectionFailure err)- where- intersectOrdered as bs = [a | a <- as, a `elem` bs]--promoteRecipientStrategy ::- RecipientPKESKVersionStrategy- -> SomeRecipientPKESKVersionStrategyW-promoteRecipientStrategy RecipientPreferV6 =- SomeRecipientPKESKVersionStrategyW RecipientPreferV6W-promoteRecipientStrategy RecipientForceV3Interop =- SomeRecipientPKESKVersionStrategyW RecipientForceV3InteropW--demoteRecipientStrategy ::- RecipientPKESKVersionStrategyW strategy- -> RecipientPKESKVersionStrategy-demoteRecipientStrategy RecipientPreferV6W = RecipientPreferV6-demoteRecipientStrategy RecipientForceV3InteropW = RecipientForceV3Interop--demoteSomeRecipientStrategy ::- SomeRecipientPKESKVersionStrategyW- -> RecipientPKESKVersionStrategy-demoteSomeRecipientStrategy (SomeRecipientPKESKVersionStrategyW strategyW) =- demoteRecipientStrategy strategyW--promoteEncryptCompatibilityProfile ::- EncryptCompatibilityProfile- -> SomeEncryptCompatibilityProfileW-promoteEncryptCompatibilityProfile EncryptStrictDefault =- SomeEncryptCompatibilityProfileW EncryptStrictDefaultW-promoteEncryptCompatibilityProfile EncryptInteropLegacy =- SomeEncryptCompatibilityProfileW EncryptInteropLegacyW---- | High-level encrypt-side helper for public-key recipient encryption.------ Returns a complete packet sequence:--- @[PKESK ..., SEIPD2 ...]@.----------buildEncryptedPacketSequence ::- SymmetricAlgorithm- -> AEADAlgorithm- -> Word8- -> RecipientPayloadShape- -> Salt- -> SessionKey- -> [Pkt]- -> B.ByteString- -> Either String [Pkt]-buildEncryptedPacketSequence symalgo aead chunkSize payloadShape salt sessionKey pkesks payload =- first renderPKESKEncryptError- (buildEncryptedPacketSequenceWithShape symalgo aead chunkSize payloadShape salt sessionKey pkesks payload)--buildEncryptedPacketSequenceWithShape ::- SymmetricAlgorithm- -> AEADAlgorithm- -> Word8- -> RecipientPayloadShape- -> Salt- -> SessionKey- -> [Pkt]- -> B.ByteString- -> Either PKESKEncryptError [Pkt]-buildEncryptedPacketSequenceWithShape symalgo aead chunkSize payloadShape salt sessionKey pkesks payload = do- onePassSignatures <- first (PayloadBuildFailure . renderOPSBuildError) (buildOnePassSignaturePackets payloadShape)- let signatures = recipientPayloadSignatures payloadShape- literalBlock =- Block- (onePassSignatures ++- [ LiteralDataPkt- (recipientPayloadDataType payloadShape)- (recipientPayloadFileName payloadShape)- (recipientPayloadTimestamp payloadShape)- (BL.fromStrict payload)- ] ++- map SignaturePkt signatures)- ciphertext <-- first PayloadBuildFailure $- encryptSEIPDv2Payload- symalgo- aead- chunkSize- salt- sessionKey- (BL.toStrict (runPut (put literalBlock)))- Right- (pkesks ++- [SymEncIntegrityProtectedDataPkt (SEIPD2 symalgo aead chunkSize salt (BL.fromStrict ciphertext))])---- | Encrypt a plaintext block with OpenPGP CFB + MDC to produce a SEIPDv1 ciphertext.-encryptSEIPDv1Payload ::- SymmetricAlgorithm- -> IV- -> SessionKey- -> B.ByteString -- ^ inner packet block plaintext- -> Either String B.ByteString-encryptSEIPDv1Payload symalgo iv (SessionKey keyBytes) plaintext =- let cleartextWithMDC = plaintext <> mdcTrailerForSEIPDv1 iv plaintext- in first- renderCipherError- (encryptOpenPGPCfbRaw OpenPGPCFBNoResyncW symalgo iv cleartextWithMDC keyBytes)---- | Build a complete RFC 4880-conformant packet sequence using SEIPDv1 (CFB + MDC).-buildEncryptedPacketSequenceWithShapeSEIPDv1 ::- SymmetricAlgorithm- -> IV- -> RecipientPayloadShape- -> SessionKey- -> [Pkt]- -> B.ByteString- -> Either PKESKEncryptError [Pkt]-buildEncryptedPacketSequenceWithShapeSEIPDv1 symalgo iv payloadShape sessionKey pkesks payload = do- onePassSignatures <- first (PayloadBuildFailure . renderOPSBuildError) (buildOnePassSignaturePackets payloadShape)- let signatures = recipientPayloadSignatures payloadShape- literalBlock =- Block- (onePassSignatures ++- [ LiteralDataPkt- (recipientPayloadDataType payloadShape)- (recipientPayloadFileName payloadShape)- (recipientPayloadTimestamp payloadShape)- (BL.fromStrict payload)- ] ++- map SignaturePkt signatures)- ciphertext <-- first PayloadBuildFailure $- encryptSEIPDv1Payload symalgo iv sessionKey (BL.toStrict (runPut (put literalBlock)))- Right- (pkesks ++- [SymEncIntegrityProtectedDataPkt (SEIPD1 1 (BL.fromStrict ciphertext))])--buildOnePassSignaturePackets :: RecipientPayloadShape -> Either OPSBuildError [Pkt]-buildOnePassSignaturePackets payloadShape- | not (recipientPayloadUseOnePassSignatures payloadShape) = Right []- | null signatures = Right []- | otherwise =- fmap (map OnePassSignaturePkt) $- sequence (zipWith buildOnePassSignature nestedFlags (reverse signatures))- where- signatures = recipientPayloadSignatures payloadShape- nestedFlags = replicate (length signatures - 1) True ++ [False]--data OnePassSignatureBuildCase where- OnePassSignatureBuildCaseV3 ::- SignaturePayloadV 'SigPayloadV3 -> OnePassSignatureBuildCase- OnePassSignatureBuildCaseV4 ::- SignaturePayloadV 'SigPayloadV4 -> OnePassSignatureBuildCase- OnePassSignatureBuildCaseV6 ::- SignaturePayloadV 'SigPayloadV6 -> OnePassSignatureBuildCase- OnePassSignatureBuildCaseOther ::- PacketVersion -> OnePassSignatureBuildCase--onePassSignatureBuildCase :: SignaturePayload -> OnePassSignatureBuildCase-onePassSignatureBuildCase sig =- case toSomeSignaturePayload sig of- SomeSignaturePayload (payload@SigPayloadV3Data {}) ->- OnePassSignatureBuildCaseV3 payload- SomeSignaturePayload (payload@SigPayloadV4Data {}) ->- OnePassSignatureBuildCaseV4 payload- SomeSignaturePayload (payload@SigPayloadV6Data {}) ->- OnePassSignatureBuildCaseV6 payload- SomeSignaturePayload (SigPayloadOtherData version _) ->- OnePassSignatureBuildCaseOther version--buildOnePassSignature :: NestedFlag -> SignaturePayload -> Either OPSBuildError OnePassSignaturePayload-buildOnePassSignature nestedFlag sig =- case onePassSignatureBuildCase sig of- OnePassSignatureBuildCaseV3 (SigPayloadV3Data sigType _ issuerKeyId pubkeyAlgo hashAlgo _ _) ->- Right- (OPSPayloadV3Packet- (OPSPayloadV3 3 sigType hashAlgo pubkeyAlgo issuerKeyId nestedFlag))- OnePassSignatureBuildCaseV4 (SigPayloadV4Data sigType pubkeyAlgo hashAlgo hashedSubpackets unhashedSubpackets _ _) ->- case signatureIssuerKeyId hashedSubpackets unhashedSubpackets of- Just issuerKeyId ->- Right- (OPSPayloadV3Packet- (OPSPayloadV3 3 sigType hashAlgo pubkeyAlgo issuerKeyId nestedFlag))- Nothing ->- Left OPSBuildMissingIssuerKeyId- OnePassSignatureBuildCaseV6 (SigPayloadV6Data sigType pubkeyAlgo hashAlgo salt hashedSubpackets unhashedSubpackets _ _) ->- case signatureIssuerFingerprint (BTypes.issuerFingerprintVersionToPacketVersion BTypes.IssuerFingerprintV6) hashedSubpackets unhashedSubpackets of- Just signerFingerprint- | BL.length signerFingerprint == 32 ->- Right- (OPSPayloadV6Packet- (OPSPayloadV6 sigType hashAlgo pubkeyAlgo salt signerFingerprint nestedFlag))- | otherwise ->- Left (OPSBuildFingerprintWrongLength (BL.length signerFingerprint))- Nothing ->- Left OPSBuildMissingIssuerFingerprint- OnePassSignatureBuildCaseOther version ->- Left (OPSBuildUnsupportedSigVersion version)--signatureIssuerKeyId :: [SigSubPacket] -> [SigSubPacket] -> Maybe EightOctetKeyId-signatureIssuerKeyId hashedSubpackets unhashedSubpackets =- case findIssuerKeyId hashedSubpackets of- Just issuerKeyId -> Just issuerKeyId- Nothing ->- case findIssuerKeyId unhashedSubpackets of- Just issuerKeyId -> Just issuerKeyId- Nothing ->- case signatureIssuerFingerprint (BTypes.issuerFingerprintVersionToPacketVersion BTypes.IssuerFingerprintV4) hashedSubpackets unhashedSubpackets of- Just issuerFingerprintBytes ->- if BL.length issuerFingerprintBytes >= 8- then- Just- (EightOctetKeyId- (BL.drop (BL.length issuerFingerprintBytes - 8) issuerFingerprintBytes))- else Nothing- Nothing -> Nothing--signatureIssuerFingerprint ::- PacketVersion -> [SigSubPacket] -> [SigSubPacket] -> Maybe BL.ByteString-signatureIssuerFingerprint expectedVersion hashedSubpackets unhashedSubpackets =- unFingerprint <$> findIssuerFingerprint expectedVersion hashedSubpackets unhashedSubpackets--findIssuerKeyId :: [SigSubPacket] -> Maybe EightOctetKeyId-findIssuerKeyId subpackets =- case find isIssuerKeyIdSubpacket subpackets of- Just (SigSubPacket _ (Issuer issuerKeyId)) -> Just issuerKeyId- _ -> Nothing--findIssuerFingerprint ::- PacketVersion -> [SigSubPacket] -> [SigSubPacket] -> Maybe Fingerprint-findIssuerFingerprint expectedVersion hashedSubpackets unhashedSubpackets =- case findIssuerFingerprintIn expectedVersion hashedSubpackets of- Just issuerFingerprint -> Just issuerFingerprint- Nothing -> findIssuerFingerprintIn expectedVersion unhashedSubpackets--findIssuerFingerprintIn :: PacketVersion -> [SigSubPacket] -> Maybe Fingerprint-findIssuerFingerprintIn expectedVersion subpackets =- case find (isIssuerFingerprintSubpacket expectedVersion) subpackets of- Just (SigSubPacket _ (IssuerFingerprint _ issuerFingerprint)) -> Just issuerFingerprint- _ -> Nothing--isIssuerKeyIdSubpacket :: SigSubPacket -> Bool-isIssuerKeyIdSubpacket (SigSubPacket _ (Issuer _)) = True-isIssuerKeyIdSubpacket _ = False--isIssuerFingerprintSubpacket :: PacketVersion -> SigSubPacket -> Bool-isIssuerFingerprintSubpacket expectedVersion (SigSubPacket _ (IssuerFingerprint version _)) =- BTypes.issuerFingerprintVersionToPacketVersion version == expectedVersion-isIssuerFingerprintSubpacket _ _ = False--buildRsaPKESKv6 ::- MonadRandom m- => SomePKPayload- -> PKESKSessionMaterial- -> m (Either PKESKEncryptError PKESKPayloadV6)-buildRsaPKESKv6 recipient material =- case _pubkey recipient of- RSAPubKey (RSA_PublicKey publicKey) -> do- encrypted <- RSA15.encrypt publicKey (pkeskEncodedSessionMaterial material)- pure $- fmap- (\esk ->- let mpiEsk = runPut (put (MPI (os2ip esk)))- in PKESKPayloadV6 (recipientKeyIdentifier recipient) RSA mpiEsk)- (first (RecipientKeyWrapFailure RSA . show) encrypted)- _ ->- pure- (Left- (InvalidRecipientKeyMaterial- RSA- "recipient PKPayload does not contain an RSA public key"))--buildRsaPKESKv3 ::- MonadRandom m- => SomePKPayload- -> PKESKV3SessionMaterial- -> m (Either PKESKEncryptError PKESKPayloadV3)-buildRsaPKESKv3 recipient material =- case _pubkey recipient of- RSAPubKey (RSA_PublicKey publicKey) ->- case eightOctetKeyID recipient of- Left err ->- pure- (Left- (InvalidRecipientKeyMaterial- (_pkalgo recipient)- ("failed to derive PKESKv3 recipient key ID: " ++ err)))- Right eoki -> do- encrypted <- RSA15.encrypt publicKey (unPKESKV3SessionMaterial material)- pure $- fmap- (\esk ->- PKESKPayloadV3- 3- eoki- (_pkalgo recipient)- (MPI (os2ip esk) :| []))- (first (RecipientKeyWrapFailure (_pkalgo recipient) . show) encrypted)- _ ->- pure- (Left- (InvalidRecipientKeyMaterial- (_pkalgo recipient)- "recipient PKPayload does not contain an RSA public key"))--buildECDHPKESKv3 ::- MonadRandom m- => SomePKPayload- -> PKESKV3SessionMaterial- -> m (Either PKESKEncryptError PKESKPayloadV3)-buildECDHPKESKv3 recipient material =- case _pubkey recipient of- ECDHPubKey ecdhPub kdfHA kdfSA ->- case eightOctetKeyID recipient of- Left err ->- pure- (Left- (InvalidRecipientKeyMaterial- ECDH- ("failed to derive PKESKv3 recipient key ID: " ++ err)))- Right eoki ->- case ecdhPub of- ECDSAPubKey (ECDSA_PublicKey recipientPub) -> do- (ephemeralPub, ephemeralPriv) <- ECCGen.generate (ECDSA.public_curve recipientPub)- case point2MBS (ECDSA.public_q ephemeralPub) of- Nothing ->- pure- (Left- (InvalidRecipientKeyMaterial- ECDH- "failed to serialize ECDH ephemeral point"))- Just ephemeralBytes ->- pure $- buildEcdhV3Payload- recipient- eoki- ECDH- ecdhPub- kdfHA- kdfSA- ephemeralBytes- (BA.convert- (ECCDH.getShared- (ECDSA.public_curve recipientPub)- (ECDSA.private_d ephemeralPriv)- (ECDSA.public_q recipientPub)) ::- B.ByteString)- material- EdDSAPubKey Ed25519 recipientPoint -> do- ephSecretRaw <- getRandomBytes 32- pure $- do recipientPublicBytes <- normalizeX25519Public (edPointBytes recipientPoint)- ephSecret <-- first (RecipientKeyWrapFailure ECDH . show) .- CE.eitherCryptoError $- C25519.secretKey (leftPadTo 32 ephSecretRaw)- recipientPub <-- first (RecipientKeyWrapFailure ECDH . show) .- CE.eitherCryptoError $- C25519.publicKey recipientPublicBytes- let ephPublicBytes =- B.cons 0x40 (BA.convert (C25519.toPublic ephSecret) :: B.ByteString)- sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString- buildEcdhV3Payload- recipient- eoki- ECDH- ecdhPub- kdfHA- kdfSA- ephPublicBytes- sharedSecret- material- _ ->- pure- (Left- (InvalidRecipientKeyMaterial- ECDH- "recipient ECDH public key is not RFC6637-compatible"))- _ ->- pure- (Left- (InvalidRecipientKeyMaterial- ECDH- "recipient PKPayload does not contain ECDH public key material"))--buildECDHPKESKv6 ::- MonadRandom m- => SomePKPayload- -> PKESKSessionMaterial- -> m (Either PKESKEncryptError PKESKPayloadV6)-buildECDHPKESKv6 recipient material =- case _pubkey recipient of- ECDHPubKey ecdhPub kdfHA kdfSA ->- case ecdhPub of- ECDSAPubKey (ECDSA_PublicKey recipientPub) -> do- (ephemeralPub, ephemeralPriv) <- ECCGen.generate (ECDSA.public_curve recipientPub)- case point2MBS (ECDSA.public_q ephemeralPub) of- Nothing ->- pure- (Left- (InvalidRecipientKeyMaterial- ECDH- "failed to serialize ECDH ephemeral point"))- Just ephemeralBytes ->- pure $- buildEcdhV6Esk- recipient- ECDH- ecdhPub- kdfHA- kdfSA- ephemeralBytes- (BA.convert- (ECCDH.getShared- (ECDSA.public_curve recipientPub)- (ECDSA.private_d ephemeralPriv)- (ECDSA.public_q recipientPub)) ::- B.ByteString)- material- EdDSAPubKey Ed25519 recipientPoint -> do- ephSecretRaw <- getRandomBytes 32- pure $- do recipientPublicBytes <- normalizeX25519Public (edPointBytes recipientPoint)- ephSecret <-- first (RecipientKeyWrapFailure ECDH . show) .- CE.eitherCryptoError $- C25519.secretKey (leftPadTo 32 ephSecretRaw)- recipientPub <-- first (RecipientKeyWrapFailure ECDH . show) .- CE.eitherCryptoError $- C25519.publicKey recipientPublicBytes- let ephPublicBytes = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString- buildEcdhV6Esk recipient ECDH ecdhPub kdfHA kdfSA ephPublicBytes sharedSecret material- EdDSAPubKey Ed448 recipientPoint -> do- ephSecretRaw <- getRandomBytes 56- pure $- do recipientPublicBytes <- normalizeX448Public (edPointBytes recipientPoint)- ephSecret <-- first (RecipientKeyWrapFailure ECDH . show) .- CE.eitherCryptoError $- C448.secretKey (leftPadTo 56 ephSecretRaw)- recipientPub <-- first (RecipientKeyWrapFailure ECDH . show) .- CE.eitherCryptoError $- C448.publicKey recipientPublicBytes- let ephPublicBytes = BA.convert (C448.toPublic ephSecret) :: B.ByteString- sharedSecret = BA.convert (C448.dh recipientPub ephSecret) :: B.ByteString- buildEcdhV6Esk recipient ECDH ecdhPub kdfHA kdfSA ephPublicBytes sharedSecret material- _ ->- pure- (Left- (InvalidRecipientKeyMaterial- ECDH- "recipient ECDH public key is not ECDSA/X25519/X448-compatible"))- _ ->- pure- (Left- (InvalidRecipientKeyMaterial- ECDH- "recipient PKPayload does not contain ECDH public key material"))--buildX25519PKESKv6 ::- MonadRandom m- => SomePKPayload- -> PKESKV6RawSessionMaterial- -> m (Either PKESKEncryptError PKESKPayloadV6)-buildX25519PKESKv6 recipient material = do- ephSecretRaw <- getRandomBytes 32- pure $- do recipientPublic <- extractX25519RecipientPublic recipient- ephSecret <-- first (RecipientKeyWrapFailure X25519 . show) .- CE.eitherCryptoError $- C25519.secretKey (leftPadTo 32 ephSecretRaw)- recipientPub <-- first (RecipientKeyWrapFailure X25519 . show) .- CE.eitherCryptoError $- C25519.publicKey recipientPublic- let ephPublicBytes = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString- kek = deriveX25519Kek ephPublicBytes recipientPublic sharedSecret- wrapped <-- first (RecipientKeyWrapFailure X25519) .- aesKeyWrapRFC3394 AES128 kek $- unPKESKV6RawSessionMaterial material- esk <- encodeV6X25519Esk ephPublicBytes wrapped- Right (PKESKPayloadV6 (recipientKeyIdentifier recipient) X25519 (BL.fromStrict esk))--buildX448PKESKv6 ::- MonadRandom m- => SomePKPayload- -> PKESKV6RawSessionMaterial- -> m (Either PKESKEncryptError PKESKPayloadV6)-buildX448PKESKv6 recipient material = do- ephSecretRaw <- getRandomBytes 56- pure $- do recipientPublic <- extractX448RecipientPublic recipient- ephSecret <-- first (RecipientKeyWrapFailure X448 . show) .- CE.eitherCryptoError $- C448.secretKey (leftPadTo 56 ephSecretRaw)- recipientPub <-- first (RecipientKeyWrapFailure X448 . show) .- CE.eitherCryptoError $- C448.publicKey recipientPublic- let ephPublicBytes = BA.convert (C448.toPublic ephSecret) :: B.ByteString- sharedSecret = BA.convert (C448.dh recipientPub ephSecret) :: B.ByteString- kek = deriveX448Kek ephPublicBytes recipientPublic sharedSecret- wrapped <-- first (RecipientKeyWrapFailure X448) .- aesKeyWrapRFC3394 AES256 kek $- unPKESKV6RawSessionMaterial material- esk <- encodeV6X448Esk ephPublicBytes wrapped- Right (PKESKPayloadV6 (recipientKeyIdentifier recipient) X448 (BL.fromStrict esk))--buildEcdhV6Esk :: SomePKPayload- -> PubKeyAlgorithm- -> PKey- -> HashAlgorithm- -> SymmetricAlgorithm- -> B.ByteString- -> B.ByteString- -> PKESKSessionMaterial- -> Either PKESKEncryptError PKESKPayloadV6-buildEcdhV6Esk recipient pka ecdhPub kdfHA kdfSA ephemeralBytes sharedSecret material = do- kdfParam <-- first (RecipientKdfFailure pka) $- buildECDHKDFParam recipient pka ecdhPub kdfHA kdfSA- kek <- first (RecipientKdfFailure pka) $ deriveECDHKek kdfHA kdfSA sharedSecret kdfParam- wrapped <-- first (RecipientKeyWrapFailure pka) .- aesKeyWrapRFC3394 kdfSA kek $- padToMultipleOf8 (pkeskEncodedSessionMaterial material)- esk <- encodeV6EcdhEsk ephemeralBytes wrapped- Right (PKESKPayloadV6 (recipientKeyIdentifier recipient) pka (BL.fromStrict esk))--buildEcdhV3Payload :: SomePKPayload- -> EightOctetKeyId- -> PubKeyAlgorithm- -> PKey- -> HashAlgorithm- -> SymmetricAlgorithm- -> B.ByteString- -> B.ByteString- -> PKESKV3SessionMaterial- -> Either PKESKEncryptError PKESKPayloadV3-buildEcdhV3Payload recipient eoki pka ecdhPub kdfHA kdfSA ephemeralBytes sharedSecret material = do- kdfParam <-- first (RecipientKdfFailure pka) $- buildECDHKDFParam recipient pka ecdhPub kdfHA kdfSA- kek <- first (RecipientKdfFailure pka) $ deriveECDHKek kdfHA kdfSA sharedSecret kdfParam- wrapped <-- first (RecipientKeyWrapFailure pka) .- aesKeyWrapRFC3394 kdfSA kek $- padToMultipleOf8 (unPKESKV3SessionMaterial material)- Right- (PKESKPayloadV3- 3- eoki- pka- (MPI (os2ip ephemeralBytes) :| [MPI (os2ip wrapped)]))--recipientKeyIdentifier :: SomePKPayload -> BL.ByteString-recipientKeyIdentifier = unFingerprint . fingerprint--encodeV6EcdhEsk :: B.ByteString -> B.ByteString -> Either PKESKEncryptError B.ByteString-encodeV6EcdhEsk ephemeral wrapped = do- let ephLen = B.length ephemeral- if ephLen > 255- then Left (RecipientKeyWrapFailure ECDH "ephemeral key encoding is too large")- else Right (B.singleton (fromIntegral ephLen) <> ephemeral <> wrapped)--encodeV6X25519Esk :: B.ByteString -> B.ByteString -> Either PKESKEncryptError B.ByteString-encodeV6X25519Esk ephemeral wrapped- | B.length ephemeral /= 32 =- Left (RecipientKeyWrapFailure X25519 "X25519 ephemeral key must be exactly 32 octets")- | B.length wrapped > 255 =- Left (RecipientKeyWrapFailure X25519 "wrapped session key encoding is too large")- | otherwise =- Right (ephemeral <> B.singleton (fromIntegral (B.length wrapped)) <> wrapped)--encodeV6X448Esk :: B.ByteString -> B.ByteString -> Either PKESKEncryptError B.ByteString-encodeV6X448Esk ephemeral wrapped- | B.length ephemeral /= 56 =- Left (RecipientKeyWrapFailure X448 "X448 ephemeral key must be exactly 56 octets")- | B.length wrapped > 255 =- Left (RecipientKeyWrapFailure X448 "wrapped session key encoding is too large")- | otherwise =- Right (ephemeral <> B.singleton (fromIntegral (B.length wrapped)) <> wrapped)--extractX25519RecipientPublic :: SomePKPayload -> Either PKESKEncryptError B.ByteString-extractX25519RecipientPublic recipient =- case _pubkey recipient of- EdDSAPubKey Ed25519 point ->- normalizeX25519Public (edPointBytes point)- ECDHPubKey (EdDSAPubKey Ed25519 point) _ _ ->- normalizeX25519Public (edPointBytes point)- other ->- Left- (InvalidRecipientKeyMaterial- X25519- ("expected X25519-compatible recipient key, got " ++ show other))--extractX448RecipientPublic :: SomePKPayload -> Either PKESKEncryptError B.ByteString-extractX448RecipientPublic recipient =- case _pubkey recipient of- EdDSAPubKey Ed448 point ->- normalizeX448Public (edPointBytes point)- ECDHPubKey (EdDSAPubKey Ed448 point) _ _ ->- normalizeX448Public (edPointBytes point)- other ->- Left- (InvalidRecipientKeyMaterial- X448- ("expected X448-compatible recipient key, got " ++ show other))--normalizeX25519Public :: B.ByteString -> Either PKESKEncryptError B.ByteString-normalizeX25519Public =- first (InvalidRecipientKeyMaterial X25519) .- normalizeMontgomeryPublic- 32- "invalid X25519 public key length/prefix: "--normalizeX448Public :: B.ByteString -> Either PKESKEncryptError B.ByteString-normalizeX448Public =- first (InvalidRecipientKeyMaterial X448) .- normalizeMontgomeryPublic- 56- "invalid X448 public key length/prefix: "--edPointBytes :: EdPoint -> B.ByteString-edPointBytes (PrefixedNativeEPoint (EPoint x)) = i2osp x-edPointBytes (NativeEPoint (EPoint x)) = i2osp x--deriveX25519Kek :: B.ByteString -> B.ByteString -> B.ByteString -> B.ByteString-deriveX25519Kek ephemeralPublic recipientPublic sharedSecret =- let ikm = ephemeralPublic <> recipientPublic <> sharedSecret- prk = extract @CHAlg.SHA256 B.empty ikm- info = "OpenPGP X25519" :: B.ByteString- in expand @CHAlg.SHA256 prk info 16--deriveX448Kek :: B.ByteString -> B.ByteString -> B.ByteString -> B.ByteString-deriveX448Kek ephemeralPublic recipientPublic sharedSecret =- let ikm = ephemeralPublic <> recipientPublic <> sharedSecret- prk = extract @CHAlg.SHA512 B.empty ikm- info = "OpenPGP X448" :: B.ByteString- in expand @CHAlg.SHA512 prk info 32--padToMultipleOf8 :: B.ByteString -> B.ByteString-padToMultipleOf8 bs- | padLen == 0 = bs- | otherwise = bs <> B.replicate padLen (fromIntegral padLen)- where- rem8 = B.length bs `mod` 8- padLen = if rem8 == 0 then 0 else 8 - rem8--checksum16 :: B.ByteString -> Word16-checksum16 =- fromIntegral .- B.foldl' (\acc octet -> (acc + fromIntegral octet) `mod` (65536 :: Integer)) 0--checksum16Bytes :: B.ByteString -> B.ByteString-checksum16Bytes bs =- B.pack- [ fromIntegral ((chk `shiftR` 8) .&. 0xff)- , fromIntegral (chk .&. 0xff)- ]- where- chk = checksum16 bs--aesKeyWrapRFC3394 ::- SymmetricAlgorithm -> B.ByteString -> B.ByteString -> Either String B.ByteString-aesKeyWrapRFC3394 sa kek plain =- withAESCipher "ECDH PKESK currently supports AES KEK algorithms only" sa kek wrapWithCipher- where- wrapWithCipher :: CCT.BlockCipher cipher => cipher -> Either String B.ByteString- wrapWithCipher cipher = do- if B.length plain < 16 || B.length plain `mod` 8 /= 0- then Left "ECDH key wrap input must be at least 16 octets and a multiple of 8"- else Right ()- let rs = chunksOf8 plain- if length rs < 2- then Left "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 => cipher -> B.ByteString -> [B.ByteString] -> Either String (B.ByteString, [B.ByteString])- wrapRounds cipher aInit rsInit = goJ 0 aInit rsInit- where- n = length rsInit- goJ j a rs- | j > 5 = Right (a, rs)- | otherwise = do- (a', rs') <- goI 1 a rs- goJ (j + 1) a' rs'- where- goI i curA curRs- | i > n = Right (curA, curRs)- | 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- aNext = xorBS msb (encodeWord64be t)- rsNext = (ix (i - 1) .~ lsb) curRs- goI (i + 1) aNext rsNext--chunksOf8 :: B.ByteString -> [B.ByteString]-chunksOf8 bs- | B.null bs = []- | otherwise =- let (h, t) = B.splitAt 8 bs- in h : chunksOf8 t--xorBS :: B.ByteString -> B.ByteString -> B.ByteString-xorBS a b = B.pack (B.zipWith xor a b)--encryptSEIPDv1WithSKESK ::- MonadRandom m- => SymmetricAlgorithm- -> S2K- -> Maybe IV- -> BL.ByteString- -> B.ByteString- -> m (Either String [Pkt])-encryptSEIPDv1WithSKESK symalgo s2k ivOverride passphrase literalPayload = do- let eSessionKey = do- keyLen <- symKeySize symalgo- first renderS2KError (string2Key s2k keyLen passphrase)- case eSessionKey of- Left err -> pure (Left err)- Right sessionKeyMaterial ->- case- first- renderCipherError- (withSymmetricCipher symalgo sessionKeyMaterial (pure . blockSize)) of- Left err -> pure (Left err)- Right ivLength -> do- ivBytes <-- maybe- (getRandomBytes ivLength)- (pure . unIV)- ivOverride- let iv = IV ivBytes- let sessionKey = SessionKey sessionKeyMaterial- case encryptSEIPDv1Payload symalgo iv sessionKey literalPayload of- Left err -> pure (Left err)- Right encrypted ->- pure- (Right- [ SKESKPkt- (SKESKPayloadV4Packet- (SKESKPayloadV4 symalgo s2k Nothing))- , SymEncIntegrityProtectedDataPkt- (SEIPD1 1 (BL.fromStrict encrypted))- ])--encryptSEIPDv2WithSKESK ::- SymmetricAlgorithm- -> AEADAlgorithm- -> Word8- -> Salt- -> S2K- -> BL.ByteString- -> B.ByteString- -> Either String [Pkt]-encryptSEIPDv2WithSKESK symalgo aead chunkSize salt s2k passphrase literalPayload = do- keyLen <- symKeySize symalgo- sessionKeyMaterial <- first renderS2KError (string2Key s2k keyLen passphrase)- (_, nonceSize) <-- aeadModeAndNonceSizeForSEIPDv2- "unsupported AEAD algorithm for SKESK v6 encrypt"- aead- when (B.length (unSalt salt) < nonceSize) $- Left "SEIPD v2 salt is too short to derive the SKESK v6 IV"- let skeskIV = B.take nonceSize (unSalt salt)- kek <- deriveSKESK6KEK symalgo aead sessionKeyMaterial- (wrappedSessionKey, skeskTag) <-- encryptSKESK6SessionKey symalgo aead kek skeskIV sessionKeyMaterial- let sessionKey = SessionKey sessionKeyMaterial- encrypted <- encryptSEIPDv2Payload symalgo aead chunkSize salt sessionKey literalPayload- return- [ SKESKPkt- (SKESKPayloadV6Packet- (SKESKPayloadV6- symalgo- aead- s2k- (BL.fromStrict skeskIV)- (BL.fromStrict wrappedSessionKey)- (BL.fromStrict skeskTag)))- , SymEncIntegrityProtectedDataPkt (SEIPD2 symalgo aead chunkSize salt (BL.fromStrict encrypted))- ]--encryptSEIPDv2WithSKESKBlock ::- SymmetricAlgorithm- -> AEADAlgorithm- -> Word8- -> Salt- -> S2K- -> BL.ByteString- -> Block Pkt- -> Either String [Pkt]-encryptSEIPDv2WithSKESKBlock symalgo aead chunkSize salt s2k passphrase packetBlock =- encryptSEIPDv2WithSKESK- symalgo- aead- chunkSize- salt- s2k- passphrase- (BL.toStrict (runPut (put packetBlock)))--encryptSEIPDv2LiteralDataWithSKESK ::- SymmetricAlgorithm- -> AEADAlgorithm- -> Word8- -> Salt- -> S2K- -> BL.ByteString- -> B.ByteString- -> Either String [Pkt]-encryptSEIPDv2LiteralDataWithSKESK symalgo aead chunkSize salt s2k passphrase payload =- encryptSEIPDv2WithSKESKBlock- symalgo- aead- chunkSize- salt- s2k- passphrase- (Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) (BL.fromStrict payload)])--encryptSEIPDv2Payload ::- SymmetricAlgorithm- -> AEADAlgorithm- -> Word8- -> Salt- -> SessionKey- -> B.ByteString- -> Either String B.ByteString-encryptSEIPDv2Payload symalgo aead chunkSize salt (SessionKey sessionKey) plaintext = do- (mode, nonceSize) <- aeadModeAndNonceSize aead- keyLen <- symKeySize symalgo- let outputLen = keyLen + nonceSize - 8- info = B.pack [0xd2, 2, fromFVal symalgo, fromFVal aead, chunkSize]- prk = extract @CHAlg.SHA256 (unSalt salt) sessionKey- okm = expand @CHAlg.SHA256 prk info outputLen :: B.ByteString- messageKey = B.take keyLen okm- noncePrefix = B.take (nonceSize - 8) (B.drop keyLen okm)- withAESCipher- "SEIPD v2 encrypt currently supports AES-128/192/256 only"- symalgo- messageKey- (encryptChunks mode info chunkSize noncePrefix plaintext)--encryptChunks ::- CCT.BlockCipher cipher- => CCT.AEADMode- -> B.ByteString- -> Word8- -> B.ByteString- -> B.ByteString- -> cipher- -> Either String B.ByteString-encryptChunks mode info chunkSize noncePrefix plaintext cipher = go 0 plaintext [] 0- where- chunkLen = 1 `shiftL` (fromIntegral chunkSize + 6)- go idx remaining acc totalPlain- | B.null remaining = do- (finalTag, finalCipher) <-- if mode == CCT.AEAD_OCB- then- encryptWithOCBRFC7253- cipher- (noncePrefix <> encodeWord64be idx)- (info <> encodeWord64be (fromIntegral totalPlain))- B.empty- else do- aead <- initAEAD idx- let (tag, out) =- CCT.aeadSimpleEncrypt- aead- (info <> encodeWord64be (fromIntegral totalPlain))- B.empty- 16- Right (tag, out)- if B.null finalCipher- then return (B.concat (reverse acc) <> authTagToBS finalTag)- else Left "expected empty ciphertext for final SEIPD v2 tag"- | otherwise = do- let (chunkPlain, rest) = B.splitAt chunkLen remaining- (tag, chunkCipher) <-- if mode == CCT.AEAD_OCB- then- encryptWithOCBRFC7253- cipher- (noncePrefix <> encodeWord64be idx)- info- chunkPlain- else do- aead <- initAEAD idx- pure (CCT.aeadSimpleEncrypt aead info chunkPlain 16)- let chunkOut = chunkCipher <> authTagToBS tag- go- (idx + 1)- rest- (chunkOut : acc)- (totalPlain + B.length chunkPlain)-- initAEAD idx =- first show . CE.eitherCryptoError $- CCT.aeadInit mode cipher (noncePrefix <> encodeWord64be idx)--aeadModeAndNonceSize :: AEADAlgorithm -> Either String (CCT.AEADMode, Int)-aeadModeAndNonceSize =- aeadModeAndNonceSizeForSEIPDv2- "unsupported AEAD algorithm for SEIPD v2 encrypt"--symKeySize :: SymmetricAlgorithm -> Either String Int-symKeySize =- seipdv2SymmetricKeySize- "unsupported symmetric algorithm for SEIPD v2 encrypt"--authTagToBS :: CCT.AuthTag -> B.ByteString-authTagToBS = BA.convert . CCT.unAuthTag--encodeWord64be :: Word64 -> B.ByteString-encodeWord64be = BL.toStrict . runPut . putWord64be---- | Compose a complete AEAD-encrypted message with optional literal data and signature.--- Returns a packet list (SKESK, SEIPD v2, optional signature) ready for serialization.------ Example: @composeMessageWithSEIPDv2 AES256 OCB 6 (Salt 32 bytes)--- (SimpleS2K SHA256) passphrase payload Nothing@--- returns @[SKESK v6, SEIPD v2, <ciphertext>]@------ If the signature is provided, it will be included in the encrypted payload.-composeMessageWithSEIPDv2 ::- SymmetricAlgorithm- -> AEADAlgorithm- -> Word8- -> Salt- -> S2K- -> BL.ByteString- -> B.ByteString- -> Maybe [Pkt]- -> Either String [Pkt]-composeMessageWithSEIPDv2 symalgo aead chunkSize salt s2k passphrase payload mSigs = do- let packets = case mSigs of- Nothing -> [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) (BL.fromStrict payload)]- Just sigs -> LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) (BL.fromStrict payload) : sigs- blockPayload = Block packets- encryptSEIPDv2WithSKESKBlock symalgo aead chunkSize salt s2k passphrase blockPayload+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}++module Codec.Encryption.OpenPGP.Encrypt+ ( PKESKEncryptError (..)+ , RecipientCapabilityNegotiationMode (..)+ , RecipientCapabilityError (..)+ , renderRecipientCapabilityError+ , RecipientCapabilities (..)+ , recipientCapabilitiesFromSubpacketPayloads+ , recipientCapabilitySupportsEncryption+ , RecipientTargetRejectionReason (..)+ , RecipientEncryptionTargetRejected (..)+ , RecipientEncryptionTargetsReport (..)+ , recipientEncryptionTargetsReportFromTKAtTimestamp+ , recipientEncryptionTargetsReportFromTK+ , recipientEncryptionTargetFromTKAtTimestamp+ , recipientEncryptionTargetFromTKAtTimestampWithPolicy+ , recipientEncryptionTargetsFromTKAtTimestamp+ , recipientEncryptionTargetFromTK+ , recipientEncryptionTargetFromTKWithPolicy+ , recipientEncryptionTargetsFromTK+ , RecipientTargetSelectionPolicy (..)+ , PassphraseSKESKVersionPolicy (..)+ , PassphraseEncryptRequest (..)+ , encryptPassphraseWithPolicy+ , PKESKVersionPolicy (..)+ , RecipientPKESKVersionStrategy (..)+ , RecipientPKESKVersionStrategyW (..)+ , SomeRecipientPKESKVersionStrategyW (..)+ , RecipientPKESKVersionSelector+ , RecipientPKESKVersionSelectorTyped+ , EncryptCompatibilityProfile (..)+ , EncryptCompatibilityProfileW (..)+ , SomeEncryptCompatibilityProfileW (..)+ , RecipientEncryptionTarget (..)+ , recipientEncryptionTarget+ , recipientEncryptionTargetWithStrategy+ , recipientEncryptionTargetWithCapabilities+ , recipientEncryptionTargetWithStrategyTyped+ , recipientVersionStrategyForProfile+ , recipientVersionStrategyForProfileTyped+ , RecipientPayloadShape (..)+ , defaultRecipientPayloadShape+ , SEIPDVersion (..)+ , RecipientEncryptResult (..)+ , RecipientEncryptRequest (..)+ , RecipientEncryptRequestOverrides (..)+ , encryptForRecipients+ , encryptForRecipientsLegacy+ , encryptForRecipientsWithCapabilityNegotiation+ , PKESKV3SessionMaterial+ , PKESKV6RawSessionMaterial+ , PKESKSessionMaterial+ , pkeskSessionAlgorithm+ , pkeskSessionKey+ , mkPKESKSessionMaterial+ , mkPKESKV3SessionMaterial+ , mkPKESKV6RawSessionMaterial+ , pkeskV3SessionMaterial+ , pkeskV6RawSessionMaterial+ , encodeOpenPGPSessionMaterial+ , generateSessionKeyMaterial+ , canonicalizePKESKRecipientId+ , canonicalizePKESKPacketRecipientIds+ , buildPKESKv3PayloadForRecipient+ , buildPKESKv3PktForRecipient+ , buildPKESKPayloadForRecipient+ , buildPKESKPktForRecipient+ , buildPKESKPktsForRecipientTargetsWithSelector+ , buildPKESKPktsForRecipientTargetsWithSelectorTyped+ , encryptSEIPDv2Payload+ , encryptSEIPDv1Payload+ , encryptSEIPDv2WithSKESK+ , encryptSEIPDv2WithSKESKBlock+ , encryptSEIPDv2LiteralDataWithSKESK+ , composeMessageWithSEIPDv2+ ) where++import Control.Applicative ((<|>))+import Control.Lens (ix, (.~))+import Control.Monad (when)+import qualified Crypto.Error as CE+import qualified Crypto.Hash.Algorithms as CHAlg+import Crypto.KDF.HKDF (expand, extract)+import Crypto.Number.Serialize (i2osp, os2ip)+import qualified Crypto.PubKey.Curve25519 as C25519+import qualified Crypto.PubKey.Curve448 as C448+import qualified Crypto.PubKey.ECC.DH as ECCDH+import qualified Crypto.PubKey.ECC.ECDSA as ECDSA+import qualified Crypto.PubKey.ECC.Generate as ECCGen+import qualified Crypto.PubKey.RSA.PKCS15 as RSA15+import Crypto.Random.Types (MonadRandom, getRandomBytes)+import Data.Bifunctor (first)+import Data.Binary (put)+import Data.Binary.Put (putWord64be, runPut)+import Data.Bits (shiftL, shiftR, xor, (.&.))+import qualified Data.ByteArray as BA+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import Data.Int (Int64)+import Data.List (find, foldl', maximumBy)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)+import Data.Ord (comparing)+import qualified Data.Set as Set+import Data.Time.Clock (UTCTime)+import Data.Time.Clock.POSIX (posixSecondsToUTCTime)+import Data.Word (Word16, Word64, Word8)+import qualified "crypton" Crypto.Cipher.Types as CCT++import Codec.Encryption.OpenPGP.BlockCipher+ ( CipherError+ , keySize+ , renderCipherError+ , withSymmetricCipher+ )+import Codec.Encryption.OpenPGP.CFB+ ( OpenPGPCFBModeW (..)+ , encryptOpenPGPCfbRaw+ , mdcTrailerForSEIPDv1+ )+import Codec.Encryption.OpenPGP.Expirations+ ( effectiveKeyPreferencesAtTimestamp+ , isPKTimeValidWithSelfSignatures+ , keyStateAt+ , keyStateValid+ , signatureEffectiveAt+ )+import Codec.Encryption.OpenPGP.Fingerprint+ ( eightOctetKeyID+ , fingerprint+ )+import Codec.Encryption.OpenPGP.Internal (leftPadTo, point2MBS)+import Codec.Encryption.OpenPGP.Internal.CryptoAES+ ( withAESCipher+ )+import Codec.Encryption.OpenPGP.Internal.CryptoECDH+ ( buildECDHKDFParam+ , deriveECDHKek+ , normalizeMontgomeryPublic+ )+import Codec.Encryption.OpenPGP.Internal.CryptoSEIPDv2+ ( aeadModeAndNonceSizeForSEIPDv2+ , deriveSKESK6KEK+ , encryptSKESK6SessionKey+ , seipdv2SymmetricKeySize+ )+import Codec.Encryption.OpenPGP.Internal.HOBlockCipher+ ( HOBlockCipher (..)+ )+import Codec.Encryption.OpenPGP.Internal.RFC7253OCB+ ( encryptWithOCBRFC7253+ )+import Codec.Encryption.OpenPGP.Ontology+ ( isSubkeyBindingSig+ , isSubkeyRevocation+ )+import Codec.Encryption.OpenPGP.Policy+ ( MessageEncryptionPolicy+ , OpenPGPRFC (..)+ , PKESKVersionPolicy (..)+ , defaultPKESKVersionPolicy+ , messageDefaultAEADAlgorithm+ , messageDefaultChunkSize+ , messageDefaultSymmetricAlgorithm+ , messageSEIPDv2SaltOctets+ , messageSEIPDv2SymmetricAlgorithms+ , policyForRFC+ , policyMessageEncryption+ )+import Codec.Encryption.OpenPGP.S2K (renderS2KError, string2Key)+import Codec.Encryption.OpenPGP.Serialize ()+import Codec.Encryption.OpenPGP.SignatureQualities+ ( sigCT+ , signatureHashedSubpacketsKnown+ )+import Codec.Encryption.OpenPGP.Types+import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as BTypes++-- | Typed failures from one-pass signature packet construction.+data OPSBuildError+ = OPSBuildMissingIssuerKeyId+ | OPSBuildMissingIssuerFingerprint+ | OPSBuildFingerprintWrongLength Int64+ | OPSBuildUnsupportedSigVersion PacketVersion+ deriving (Eq, Show)++renderOPSBuildError :: OPSBuildError -> String+renderOPSBuildError OPSBuildMissingIssuerKeyId =+ "cannot build OPS3 packet from v4 signature without issuer metadata"+renderOPSBuildError OPSBuildMissingIssuerFingerprint =+ "cannot build OPS6 packet from v6 signature without issuer fingerprint"+renderOPSBuildError (OPSBuildFingerprintWrongLength n) =+ "cannot build OPS6 packet: issuer fingerprint must be 32 octets, got "+ ++ show n+renderOPSBuildError (OPSBuildUnsupportedSigVersion v) =+ "cannot build one-pass signature packet for unsupported signature version "+ ++ show v++-- | Typed failures surfaced by encrypt-side PKESK and SEIPD-v2 helpers.+data PKESKEncryptError+ = UnsupportedSessionKeyAlgorithm SymmetricAlgorithm String+ | InvalidSessionKeyLength SymmetricAlgorithm Int Int+ | InvalidRecipientIdentifier String+ | UnsupportedRecipientAlgorithm PubKeyAlgorithm+ | InvalidRecipientKeyMaterial PubKeyAlgorithm String+ | RecipientKdfFailure PubKeyAlgorithm String+ | RecipientKeyWrapFailure PubKeyAlgorithm String+ | RecipientCapabilitySelectionFailure RecipientCapabilityError+ | PayloadBuildFailure String+ | NoRecipientsProvided+ deriving (Eq, Show)++renderPKESKEncryptError :: PKESKEncryptError -> String+renderPKESKEncryptError (UnsupportedSessionKeyAlgorithm algo reason) =+ "unsupported session key algorithm "+ ++ show algo+ ++ ": "+ ++ reason+renderPKESKEncryptError (InvalidSessionKeyLength algo expected actual) =+ "invalid session key length for "+ ++ show algo+ ++ ": expected "+ ++ show expected+ ++ ", got "+ ++ show actual+renderPKESKEncryptError (InvalidRecipientIdentifier reason) =+ "invalid recipient identifier: " ++ reason+renderPKESKEncryptError (UnsupportedRecipientAlgorithm algo) =+ "unsupported recipient public-key algorithm: " ++ show algo+renderPKESKEncryptError (InvalidRecipientKeyMaterial algo reason) =+ "invalid recipient key material for "+ ++ show algo+ ++ ": "+ ++ reason+renderPKESKEncryptError (RecipientKdfFailure algo reason) =+ "KDF failure for recipient algorithm "+ ++ show algo+ ++ ": "+ ++ reason+renderPKESKEncryptError (RecipientKeyWrapFailure algo reason) =+ "key wrap failure for recipient algorithm "+ ++ show algo+ ++ ": "+ ++ reason+renderPKESKEncryptError (RecipientCapabilitySelectionFailure err) =+ renderRecipientCapabilityError err+renderPKESKEncryptError (PayloadBuildFailure reason) =+ "payload build failure: " ++ reason+renderPKESKEncryptError NoRecipientsProvided =+ "no recipients provided"++data RecipientCapabilityNegotiationMode+ = RecipientCapabilityNegotiationOff+ | RecipientCapabilityNegotiationOn+ deriving (Eq, Show)++data RecipientCapabilityError+ = RecipientCapabilityMissingEncryptionFlags+ SomePKPayload+ (Set.Set KeyFlag)+ | RecipientCapabilityNoEncryptableKeyMaterialInTK+ | RecipientCapabilityMissingSEIPDv1Support [SomePKPayload]+ | RecipientCapabilityMissingSEIPDv2Support [SomePKPayload]+ | RecipientCapabilityNoCommonSymmetricAlgorithms+ [SymmetricAlgorithm]+ | RecipientCapabilityNoCommonAEADAlgorithms [AEADAlgorithm]+ deriving (Eq, Show)++renderRecipientCapabilityError+ :: RecipientCapabilityError -> String+renderRecipientCapabilityError (RecipientCapabilityMissingEncryptionFlags recipient flags) =+ "recipient "+ ++ show (_keyVersion recipient, _pkalgo recipient)+ ++ " does not advertise encryption-capable key flags; observed flags: "+ ++ show (Set.toList flags)+renderRecipientCapabilityError RecipientCapabilityNoEncryptableKeyMaterialInTK =+ "no encryption-capable primary key or subkey was found in transferable key material"+renderRecipientCapabilityError (RecipientCapabilityMissingSEIPDv1Support recipients) =+ "recipient set does not advertise SEIPDv1 (MDC) support: "+ ++ show (map (\r -> (_keyVersion r, _pkalgo r)) recipients)+renderRecipientCapabilityError (RecipientCapabilityMissingSEIPDv2Support recipients) =+ "recipient set does not advertise SEIPDv2 support: "+ ++ show (map (\r -> (_keyVersion r, _pkalgo r)) recipients)+renderRecipientCapabilityError (RecipientCapabilityNoCommonSymmetricAlgorithms syms) =+ "no common recipient-supported symmetric algorithms: "+ ++ show syms+renderRecipientCapabilityError (RecipientCapabilityNoCommonAEADAlgorithms aeads) =+ "no common recipient-supported AEAD algorithms: " ++ show aeads++data RecipientCapabilities+ = RecipientCapabilities+ { recipientCapabilityKeyVersion :: KeyVersion+ , recipientCapabilityPublicKeyAlgorithm :: PubKeyAlgorithm+ , recipientCapabilityKeyFlags :: Set.Set KeyFlag+ , recipientCapabilityFeatures :: Set.Set FeatureFlag+ , recipientCapabilityPreferredSymmetricAlgorithms+ :: [SymmetricAlgorithm]+ , recipientCapabilityPreferredAEADAlgorithms :: [AEADAlgorithm]+ }+ deriving (Eq, Show)++data RecipientTargetRejectionReason+ = RecipientTargetUnsupportedAlgorithm PubKeyAlgorithm+ | RecipientTargetMissingEncryptionFlags+ SomePKPayload+ (Set.Set KeyFlag)+ | RecipientTargetRevoked SomePKPayload+ | RecipientTargetNotValidAtTimestamp+ SomePKPayload+ ThirtyTwoBitTimeStamp+ deriving (Eq, Show)++data RecipientEncryptionTargetRejected+ = RecipientEncryptionTargetRejected+ { recipientEncryptionTargetRejectedKey :: SomePKPayload+ , recipientEncryptionTargetRejectedCapabilities+ :: Maybe RecipientCapabilities+ , recipientEncryptionTargetRejectedReason+ :: RecipientTargetRejectionReason+ }+ deriving (Eq, Show)++data RecipientEncryptionTargetsReport+ = RecipientEncryptionTargetsReport+ { recipientEncryptionTargetsAccepted :: [RecipientEncryptionTarget]+ , recipientEncryptionTargetsRejected+ :: [RecipientEncryptionTargetRejected]+ }+ deriving (Eq, Show)++{- | Extract encrypt-relevant recipient capabilities from effective+self-signature subpackets.++RFC 9580 preferred AEAD ciphersuites are currently carried through+'OtherSigSub' type 39 and decoded into AEAD preferences here.+-}+recipientCapabilitiesFromSubpacketPayloads+ :: SomePKPayload+ -> [SigSubPacketPayload]+ -> RecipientCapabilities+recipientCapabilitiesFromSubpacketPayloads recipient payloads =+ foldl' step (emptyRecipientCapabilities recipient) payloads+ where+ preferredAEADCiphersuitesSubpacketType :: Word8+ preferredAEADCiphersuitesSubpacketType = 39++ step caps payload =+ case payload of+ KeyFlags flags ->+ caps+ { recipientCapabilityKeyFlags =+ recipientCapabilityKeyFlags caps `Set.union` flags+ }+ Features features ->+ caps+ { recipientCapabilityFeatures =+ recipientCapabilityFeatures caps `Set.union` features+ }+ PreferredSymmetricAlgorithms syms ->+ caps+ { recipientCapabilityPreferredSymmetricAlgorithms =+ recipientCapabilityPreferredSymmetricAlgorithms caps ++ syms+ }+ PreferredAEADCiphersuites ciphersuites ->+ caps+ { recipientCapabilityPreferredAEADAlgorithms =+ recipientCapabilityPreferredAEADAlgorithms caps+ ++ preferredAEADAlgorithmsFromCiphersuitePairs ciphersuites+ }+ OtherSigSub subpacketType rawPayload+ | subpacketType == preferredAEADCiphersuitesSubpacketType ->+ caps+ { recipientCapabilityPreferredAEADAlgorithms =+ recipientCapabilityPreferredAEADAlgorithms caps+ ++ preferredAEADAlgorithmsFromCiphersuites rawPayload+ }+ _ -> caps++ emptyRecipientCapabilities key =+ RecipientCapabilities+ { recipientCapabilityKeyVersion = _keyVersion key+ , recipientCapabilityPublicKeyAlgorithm = _pkalgo key+ , recipientCapabilityKeyFlags = Set.empty+ , recipientCapabilityFeatures = Set.empty+ , recipientCapabilityPreferredSymmetricAlgorithms = []+ , recipientCapabilityPreferredAEADAlgorithms = []+ }++ preferredAEADAlgorithmsFromCiphersuites+ :: BL.ByteString -> [AEADAlgorithm]+ preferredAEADAlgorithmsFromCiphersuites =+ dedupePreservingOrder . parsePairs . BL.unpack+ where+ parsePairs (_symAlgo : aeadAlgo : rest) =+ (toFVal aeadAlgo :: AEADAlgorithm) : parsePairs rest+ parsePairs _ = []++ dedupePreservingOrder = foldl' addIfMissing []+ addIfMissing acc x+ | x `elem` acc = acc+ | otherwise = acc ++ [x]++ preferredAEADAlgorithmsFromCiphersuitePairs+ :: [(SymmetricAlgorithm, AEADAlgorithm)] -> [AEADAlgorithm]+ preferredAEADAlgorithmsFromCiphersuitePairs =+ dedupePreservingOrder . map snd+ where+ dedupePreservingOrder = foldl' addIfMissing []+ addIfMissing acc x+ | x `elem` acc = acc+ | otherwise = acc ++ [x]++recipientCapabilitySupportsEncryption+ :: RecipientCapabilities -> Bool+recipientCapabilitySupportsEncryption caps =+ let flags = recipientCapabilityKeyFlags caps+ in Set.null flags+ || Set.member EncryptStorageKey flags+ || Set.member EncryptCommunicationsKey flags++recipientCapabilityAdvertisesSEIPDv1Support+ :: RecipientCapabilities -> Bool+recipientCapabilityAdvertisesSEIPDv1Support caps =+ let features = recipientCapabilityFeatures caps+ in Set.null features || Set.member FeatureSEIPDv1 features++recipientCapabilityAdvertisesSEIPDv2Support+ :: RecipientCapabilities -> Bool+recipientCapabilityAdvertisesSEIPDv2Support caps =+ recipientCapabilityAdvertisesSEIPDv1Support caps+ && Set.member FeatureSEIPDv2 (recipientCapabilityFeatures caps)++recipientEncryptionTargetFromTKAtTimestamp+ :: ThirtyTwoBitTimeStamp+ -> TKUnknown+ -> Either RecipientCapabilityError RecipientEncryptionTarget+recipientEncryptionTargetFromTKAtTimestamp timestamp tk =+ recipientEncryptionTargetFromTKAtTimestampWithPolicy+ RecipientTargetSelectionFirstValid+ timestamp+ tk++data RecipientTargetSelectionPolicy+ = RecipientTargetSelectionFirstValid+ | RecipientTargetSelectionPreferPrimary+ | RecipientTargetSelectionPreferSubkey+ | RecipientTargetSelectionPreferNewestCreationTime+ deriving (Eq, Show)++recipientEncryptionTargetFromTKAtTimestampWithPolicy+ :: RecipientTargetSelectionPolicy+ -> ThirtyTwoBitTimeStamp+ -> TKUnknown+ -> Either RecipientCapabilityError RecipientEncryptionTarget+recipientEncryptionTargetFromTKAtTimestampWithPolicy policy timestamp tk =+ case chooseRecipientTarget policy tk acceptedTargets of+ Just target -> Right target+ Nothing -> Left RecipientCapabilityNoEncryptableKeyMaterialInTK+ where+ acceptedTargets =+ recipientEncryptionTargetsAccepted+ (recipientEncryptionTargetsReportFromTKAtTimestamp timestamp tk)++recipientEncryptionTargetFromTK+ :: TK 'PublicTK+ -> Either RecipientCapabilityError RecipientEncryptionTarget+recipientEncryptionTargetFromTK tk =+ recipientEncryptionTargetFromTKAtTimestampWithPolicy+ RecipientTargetSelectionFirstValid+ (_timestamp (keyPktPKPayload (_tkPrimaryKey tk)))+ (tkToUnknown tk)++recipientEncryptionTargetFromTKWithPolicy+ :: RecipientTargetSelectionPolicy+ -> TK 'PublicTK+ -> Either RecipientCapabilityError RecipientEncryptionTarget+recipientEncryptionTargetFromTKWithPolicy policy tk =+ recipientEncryptionTargetFromTKAtTimestampWithPolicy+ policy+ (_timestamp (keyPktPKPayload (_tkPrimaryKey tk)))+ (tkToUnknown tk)++recipientEncryptionTargetsFromTKAtTimestamp+ :: ThirtyTwoBitTimeStamp+ -> TKUnknown+ -> [RecipientEncryptionTarget]+recipientEncryptionTargetsFromTKAtTimestamp timestamp tk =+ recipientEncryptionTargetsAccepted+ (recipientEncryptionTargetsReportFromTKAtTimestamp timestamp tk)++recipientEncryptionTargetsReportFromTKAtTimestamp+ :: ThirtyTwoBitTimeStamp+ -> TKUnknown+ -> RecipientEncryptionTargetsReport+recipientEncryptionTargetsReportFromTKAtTimestamp timestamp tk =+ foldr+ classifyCandidate+ emptyReport+ (subkeyCandidates ++ [primaryCandidate])+ where+ emptyReport = RecipientEncryptionTargetsReport [] []+ primaryCandidate = fst (_tkuKey tk)+ primaryPreferencePayloads =+ fromMaybe [] (effectiveKeyPreferencesAtTimestamp timestamp tk)+ subkeyCandidates =+ mapMaybe+ ( \(pkt, _) ->+ case pkt of+ PublicSubkeyPkt pkp -> Just pkp+ SecretSubkeyPkt pkp _ -> Just pkp+ _ -> Nothing+ )+ (_tkuSubs tk)+ classifyCandidate key report =+ let caps =+ recipientCapabilitiesFromSubpacketPayloads+ key+ ( primaryPreferencePayloads+ ++ subkeyBindingCapabilityPayloads timestamp tk key+ )+ keyStateRejection = recipientValidityRejectionReason timestamp tk key+ in case keyStateRejection of+ Just rejectionReason ->+ report+ { recipientEncryptionTargetsRejected =+ RecipientEncryptionTargetRejected+ { recipientEncryptionTargetRejectedKey = key+ , recipientEncryptionTargetRejectedCapabilities = Just caps+ , recipientEncryptionTargetRejectedReason = rejectionReason+ }+ : recipientEncryptionTargetsRejected report+ }+ Nothing ->+ if not (supportsPKESKRecipientAlgorithm key)+ then+ report+ { recipientEncryptionTargetsRejected =+ RecipientEncryptionTargetRejected+ { recipientEncryptionTargetRejectedKey = key+ , recipientEncryptionTargetRejectedCapabilities = Just caps+ , recipientEncryptionTargetRejectedReason =+ RecipientTargetUnsupportedAlgorithm (_pkalgo key)+ }+ : recipientEncryptionTargetsRejected report+ }+ else+ if recipientCapabilitySupportsEncryption caps+ then+ report+ { recipientEncryptionTargetsAccepted =+ recipientEncryptionTargetWithCapabilities key caps+ : recipientEncryptionTargetsAccepted report+ }+ else+ report+ { recipientEncryptionTargetsRejected =+ RecipientEncryptionTargetRejected+ { recipientEncryptionTargetRejectedKey = key+ , recipientEncryptionTargetRejectedCapabilities = Just caps+ , recipientEncryptionTargetRejectedReason =+ RecipientTargetMissingEncryptionFlags+ key+ (recipientCapabilityKeyFlags caps)+ }+ : recipientEncryptionTargetsRejected report+ }++recipientEncryptionTargetsFromTK+ :: TK 'PublicTK -> [RecipientEncryptionTarget]+recipientEncryptionTargetsFromTK tk =+ recipientEncryptionTargetsFromTKAtTimestamp+ (_timestamp (keyPktPKPayload (_tkPrimaryKey tk)))+ (tkToUnknown tk)++recipientEncryptionTargetsReportFromTK+ :: TK 'PublicTK -> RecipientEncryptionTargetsReport+recipientEncryptionTargetsReportFromTK tk =+ recipientEncryptionTargetsReportFromTKAtTimestamp+ (_timestamp (keyPktPKPayload (_tkPrimaryKey tk)))+ (tkToUnknown tk)++subkeyBindingCapabilityPayloads+ :: ThirtyTwoBitTimeStamp+ -> TKUnknown+ -> SomePKPayload+ -> [SigSubPacketPayload]+subkeyBindingCapabilityPayloads timestamp tk recipient =+ maybe [] latestEffectiveBindingPayloads matchingSubkey+ where+ matchingSubkey =+ find+ ( \(pkt, _) ->+ case pkt of+ PublicSubkeyPkt pkp -> fingerprint pkp == fingerprint recipient+ SecretSubkeyPkt pkp _ -> fingerprint pkp == fingerprint recipient+ _ -> False+ )+ (_tkuSubs tk)+ latestEffectiveBindingPayloads (_, sigs) =+ maybe+ []+ signaturePayloadsFromSignature+ (latestEffectiveSubkeyBindingSignature timestamp sigs)++latestEffectiveSubkeyBindingSignature+ :: ThirtyTwoBitTimeStamp+ -> [SignaturePayload]+ -> Maybe SignaturePayload+latestEffectiveSubkeyBindingSignature timestamp sigs =+ case filter (isEffectiveSubkeyBindingSignature timestamp) sigs of+ [] -> Nothing+ candidates ->+ Just+ (maximumBy (comparing signatureCreationTimestamp) candidates)++isEffectiveSubkeyBindingSignature+ :: ThirtyTwoBitTimeStamp+ -> SignaturePayload+ -> Bool+isEffectiveSubkeyBindingSignature timestamp sig =+ isSubkeyBindingSig sig+ && maybe+ False+ ( \created ->+ let tsValue = toInteger (unThirtyTwoBitTimeStamp timestamp)+ createdValue = toInteger (unThirtyTwoBitTimeStamp created)+ in createdValue <= tsValue+ && maybe+ True+ ( \duration ->+ if unThirtyTwoBitDuration duration == 0+ then True+ else+ tsValue+ < createdValue + toInteger (unThirtyTwoBitDuration duration)+ )+ (signatureExpirationDuration sig)+ )+ (sigCT sig)++signatureCreationTimestamp+ :: SignaturePayload -> ThirtyTwoBitTimeStamp+signatureCreationTimestamp sig =+ fromMaybe (ThirtyTwoBitTimeStamp 0) (sigCT sig)++signatureExpirationDuration+ :: SignaturePayload -> Maybe ThirtyTwoBitDuration+signatureExpirationDuration sig =+ case signatureHashedSubpacketsKnown sig of+ Just hashed ->+ foldr+ ( \subpacket acc ->+ case subpacket of+ SigSubPacket _ (SigExpirationTime duration) -> Just duration+ _ -> acc+ )+ Nothing+ hashed+ Nothing -> Nothing++signaturePayloadsFromSignature+ :: SignaturePayload -> [SigSubPacketPayload]+signaturePayloadsFromSignature sig =+ case signatureHashedSubpacketsKnown sig of+ Just hashed -> map (\(SigSubPacket _ payload) -> payload) hashed+ Nothing -> []++recipientValidityRejectionReason+ :: ThirtyTwoBitTimeStamp+ -> TKUnknown+ -> SomePKPayload+ -> Maybe RecipientTargetRejectionReason+recipientValidityRejectionReason timestamp tk key+ | fingerprint key == fingerprint (fst (_tkuKey tk)) =+ if keyStateValid (keyStateAt (timestampToUTC timestamp) tk)+ then Nothing+ else Just (RecipientTargetNotValidAtTimestamp key timestamp)+ | otherwise =+ case findMatchingSubkeySignatures tk key of+ Nothing -> Nothing+ Just sigs+ | subkeyRevokedAtTimestamp timestamp sigs ->+ Just (RecipientTargetRevoked key)+ | isPKTimeValidWithSelfSignatures+ (timestampToUTC timestamp)+ key+ sigs ->+ Nothing+ | otherwise ->+ Just (RecipientTargetNotValidAtTimestamp key timestamp)++findMatchingSubkeySignatures+ :: TKUnknown -> SomePKPayload -> Maybe [SignaturePayload]+findMatchingSubkeySignatures tk recipient =+ snd+ <$> find+ ( \(pkt, _) ->+ case pkt of+ PublicSubkeyPkt pkp -> fingerprint pkp == fingerprint recipient+ SecretSubkeyPkt pkp _ -> fingerprint pkp == fingerprint recipient+ _ -> False+ )+ (_tkuSubs tk)++subkeyRevokedAtTimestamp+ :: ThirtyTwoBitTimeStamp -> [SignaturePayload] -> Bool+subkeyRevokedAtTimestamp timestamp =+ any+ ( \sig ->+ isSubkeyRevocation sig+ && signatureEffectiveAt (timestampToUTC timestamp) sig+ )++timestampToUTC :: ThirtyTwoBitTimeStamp -> UTCTime+timestampToUTC =+ posixSecondsToUTCTime . realToFrac . unThirtyTwoBitTimeStamp++supportsPKESKRecipientAlgorithm :: SomePKPayload -> Bool+supportsPKESKRecipientAlgorithm recipient =+ case _pkalgo recipient of+ RSA -> True+ DeprecatedRSAEncryptOnly -> True+ ECDH -> True+ X25519 -> True+ X448 -> True+ _ -> False++chooseRecipientTarget+ :: RecipientTargetSelectionPolicy+ -> TKUnknown+ -> [RecipientEncryptionTarget]+ -> Maybe RecipientEncryptionTarget+chooseRecipientTarget policy tk targets =+ case policy of+ RecipientTargetSelectionFirstValid -> listToMaybe targets+ RecipientTargetSelectionPreferPrimary ->+ listToMaybe (filter (isPrimaryTarget tk) targets)+ <|> listToMaybe targets+ RecipientTargetSelectionPreferSubkey ->+ listToMaybe (filter (not . isPrimaryTarget tk) targets)+ <|> listToMaybe targets+ RecipientTargetSelectionPreferNewestCreationTime ->+ case targets of+ [] -> Nothing+ (target : rest) ->+ Just+ ( foldl'+ ( \best candidate ->+ if _timestamp (recipientEncryptionTargetKey candidate)+ > _timestamp (recipientEncryptionTargetKey best)+ then candidate+ else best+ )+ target+ rest+ )+ where+ isPrimaryTarget currentTK target =+ fingerprint (recipientEncryptionTargetKey target)+ == fingerprint (fst (_tkuKey currentTK))++-- | Session-key bundle for PKESK/SKESK packet construction.+newtype PKESKV3SessionMaterial+ = PKESKV3SessionMaterial+ { unPKESKV3SessionMaterial :: B.ByteString+ }+ deriving (Eq, Show)++newtype PKESKV6RawSessionMaterial+ = PKESKV6RawSessionMaterial+ { unPKESKV6RawSessionMaterial :: B.ByteString+ }+ deriving (Eq, Show)++data PKESKSessionMaterial+ = PKESKSessionMaterial+ { pkeskSessionAlgorithm :: SymmetricAlgorithm+ , pkeskSessionKey :: SessionKey+ , pkeskEncodedSessionMaterial :: B.ByteString+ }+ deriving (Eq, Show)++mkPKESKSessionMaterial+ :: SymmetricAlgorithm+ -> SessionKey+ -> Either PKESKEncryptError PKESKSessionMaterial+mkPKESKSessionMaterial symalgo sessionKey = do+ v3Material <- mkPKESKV3SessionMaterial symalgo sessionKey+ _v6Material <- mkPKESKV6RawSessionMaterial symalgo sessionKey+ pure+ PKESKSessionMaterial+ { pkeskSessionAlgorithm = symalgo+ , pkeskSessionKey = sessionKey+ , pkeskEncodedSessionMaterial = unPKESKV3SessionMaterial v3Material+ }++mkPKESKV3SessionMaterial+ :: SymmetricAlgorithm+ -> SessionKey+ -> Either PKESKEncryptError PKESKV3SessionMaterial+mkPKESKV3SessionMaterial symalgo sessionKey = do+ keyBytes <- validatedSessionKeyBytes symalgo sessionKey+ pure $+ PKESKV3SessionMaterial+ ( B.singleton (fromFVal symalgo)+ <> keyBytes+ <> checksum16Bytes keyBytes+ )++mkPKESKV6RawSessionMaterial+ :: SymmetricAlgorithm+ -> SessionKey+ -> Either PKESKEncryptError PKESKV6RawSessionMaterial+mkPKESKV6RawSessionMaterial symalgo sessionKey =+ PKESKV6RawSessionMaterial+ <$> validatedSessionKeyBytes symalgo sessionKey++pkeskV3SessionMaterial+ :: PKESKSessionMaterial -> PKESKV3SessionMaterial+pkeskV3SessionMaterial =+ PKESKV3SessionMaterial . pkeskEncodedSessionMaterial++pkeskV6RawSessionMaterial+ :: PKESKSessionMaterial -> PKESKV6RawSessionMaterial+pkeskV6RawSessionMaterial =+ PKESKV6RawSessionMaterial . unSessionKey . pkeskSessionKey++validatedSessionKeyBytes+ :: SymmetricAlgorithm+ -> SessionKey+ -> Either PKESKEncryptError B.ByteString+validatedSessionKeyBytes symalgo (SessionKey sessionKey) = do+ keyLen <-+ first+ (UnsupportedSessionKeyAlgorithm symalgo . renderCipherError)+ (keySize symalgo)+ let actualLen = B.length sessionKey+ if actualLen /= keyLen+ then Left (InvalidSessionKeyLength symalgo keyLen actualLen)+ else Right sessionKey++data RecipientPKESKVersionStrategy+ = RecipientPreferV6+ | RecipientForceV3Interop+ deriving (Eq, Show)++data+ RecipientPKESKVersionStrategyW+ (strategy :: RecipientPKESKVersionStrategy)+ where+ RecipientPreferV6W+ :: RecipientPKESKVersionStrategyW 'RecipientPreferV6+ RecipientForceV3InteropW+ :: RecipientPKESKVersionStrategyW 'RecipientForceV3Interop++data SomeRecipientPKESKVersionStrategyW where+ SomeRecipientPKESKVersionStrategyW+ :: RecipientPKESKVersionStrategyW strategy+ -> SomeRecipientPKESKVersionStrategyW++type RecipientPKESKVersionSelector =+ SomePKPayload+ -> Either PKESKEncryptError RecipientPKESKVersionStrategy++type RecipientPKESKVersionSelectorTyped =+ SomePKPayload+ -> Either PKESKEncryptError SomeRecipientPKESKVersionStrategyW++data EncryptCompatibilityProfile+ = EncryptStrictDefault+ | EncryptInteropLegacy+ deriving (Eq, Show)++data+ EncryptCompatibilityProfileW+ (profile :: EncryptCompatibilityProfile)+ where+ EncryptStrictDefaultW+ :: EncryptCompatibilityProfileW 'EncryptStrictDefault+ EncryptInteropLegacyW+ :: EncryptCompatibilityProfileW 'EncryptInteropLegacy++data SomeEncryptCompatibilityProfileW where+ SomeEncryptCompatibilityProfileW+ :: EncryptCompatibilityProfileW profile+ -> SomeEncryptCompatibilityProfileW++data SEIPDVersion+ = SEIPDv1+ | SEIPDv2+ deriving (Eq, Show)++type family+ PayloadVersionForProfile (profile :: EncryptCompatibilityProfile)+ :: SEIPDVersion+ where+ PayloadVersionForProfile 'EncryptStrictDefault = 'SEIPDv2+ PayloadVersionForProfile 'EncryptInteropLegacy = 'SEIPDv1++type family+ ProfileForPayloadVersion (version :: SEIPDVersion)+ :: EncryptCompatibilityProfile+ where+ ProfileForPayloadVersion 'SEIPDv1 = 'EncryptInteropLegacy+ ProfileForPayloadVersion 'SEIPDv2 = 'EncryptStrictDefault++data RecipientEncryptionTarget+ = RecipientEncryptionTarget+ { recipientEncryptionTargetKey :: SomePKPayload+ -- ^ Recipient key packet selected for PKESK wrapping.+ , recipientEncryptionTargetStrategy+ :: Maybe RecipientPKESKVersionStrategy+ {- ^ Optional explicit PKESK version strategy hint.+ When absent, profile defaults and auto-detection apply.+ -}+ , recipientEncryptionTargetCapabilities+ :: Maybe RecipientCapabilities+ {- ^ Optional recipient capability hints used by negotiation-enabled+ encryption to choose common symmetric/AEAD algorithms.+ -}+ }+ deriving (Eq, Show)++recipientEncryptionTarget+ :: SomePKPayload -> RecipientEncryptionTarget+recipientEncryptionTarget recipient =+ RecipientEncryptionTarget recipient Nothing Nothing++recipientEncryptionTargetWithStrategy+ :: SomePKPayload+ -> RecipientPKESKVersionStrategy+ -> RecipientEncryptionTarget+recipientEncryptionTargetWithStrategy recipient strategy =+ RecipientEncryptionTarget recipient (Just strategy) Nothing++recipientEncryptionTargetWithCapabilities+ :: SomePKPayload+ -> RecipientCapabilities+ -> RecipientEncryptionTarget+recipientEncryptionTargetWithCapabilities recipient capabilities =+ RecipientEncryptionTarget recipient Nothing (Just capabilities)++recipientEncryptionTargetWithStrategyTyped+ :: SomePKPayload+ -> RecipientPKESKVersionStrategyW strategy+ -> RecipientEncryptionTarget+recipientEncryptionTargetWithStrategyTyped recipient strategyW =+ recipientEncryptionTargetWithStrategy+ recipient+ (demoteRecipientStrategy strategyW)++recipientVersionStrategyForProfile+ :: EncryptCompatibilityProfile+ -> RecipientEncryptionTarget+ -> Either PKESKEncryptError RecipientPKESKVersionStrategy+recipientVersionStrategyForProfile profile target =+ case promoteEncryptCompatibilityProfile profile of+ SomeEncryptCompatibilityProfileW profileW ->+ demoteSomeRecipientStrategy+ <$> recipientVersionStrategyForProfileTyped profileW target++recipientVersionStrategyForProfileTyped+ :: EncryptCompatibilityProfileW profile+ -> RecipientEncryptionTarget+ -> Either PKESKEncryptError SomeRecipientPKESKVersionStrategyW+recipientVersionStrategyForProfileTyped profile target =+ Right $+ case recipientEncryptionTargetStrategy target of+ Just strategy ->+ promoteRecipientStrategy strategy+ Nothing ->+ case profile of+ EncryptStrictDefaultW ->+ autoDetectRecipientVersionStrategy+ (recipientEncryptionTargetKey target)+ EncryptInteropLegacyW ->+ SomeRecipientPKESKVersionStrategyW RecipientForceV3InteropW++profileForPayloadVersionW+ :: RecipientEncryptRequestOverrides version+ -> EncryptCompatibilityProfileW (ProfileForPayloadVersion version)+profileForPayloadVersionW overrides =+ case overrides of+ RecipientEncryptRequestSEIPDv2Overrides {} -> EncryptStrictDefaultW+ RecipientEncryptRequestSEIPDv1Overrides {} -> EncryptInteropLegacyW++autoDetectRecipientVersionStrategy+ :: SomePKPayload+ -> SomeRecipientPKESKVersionStrategyW+autoDetectRecipientVersionStrategy recipient+ | _keyVersion recipient == V6 =+ SomeRecipientPKESKVersionStrategyW RecipientPreferV6W+ | _pkalgo recipient `elem` [X25519, X448] =+ SomeRecipientPKESKVersionStrategyW RecipientPreferV6W+ | otherwise =+ SomeRecipientPKESKVersionStrategyW RecipientForceV3InteropW++data RecipientPayloadShape+ = RecipientPayloadShape+ { recipientPayloadDataType :: LiteralDataType+ , recipientPayloadFileName :: FileName+ , recipientPayloadTimestamp :: ThirtyTwoBitTimeStamp+ , recipientPayloadUseOnePassSignatures :: Bool+ , recipientPayloadSignatures :: [SignaturePayload]+ }+ deriving (Eq, Show)++data PassphraseSKESKVersionPolicy+ = PassphraseSKESKPreferV6+ | PassphraseSKESKForceV4Interop+ deriving (Eq, Show)++data PassphraseEncryptRequest+ = PassphraseEncryptRequest+ { passphraseEncryptVersionPolicy :: PassphraseSKESKVersionPolicy+ , passphraseEncryptSymmetricAlgorithm :: SymmetricAlgorithm+ , passphraseEncryptS2K :: S2K+ , passphraseEncryptPassphrase :: BL.ByteString+ , passphraseEncryptPayload :: B.ByteString+ , passphraseEncryptSEIPDv1IVOverride :: Maybe IV+ , passphraseEncryptSEIPDv2AEADOverride :: Maybe AEADAlgorithm+ , passphraseEncryptSEIPDv2ChunkSizeOverride :: Maybe Word8+ , passphraseEncryptSEIPDv2SaltOverride :: Maybe Salt+ }+ deriving (Eq, Show)++encryptPassphraseWithPolicy+ :: MonadRandom m+ => PassphraseEncryptRequest+ -> m (Either String [Pkt])+encryptPassphraseWithPolicy request =+ case passphraseEncryptVersionPolicy request of+ PassphraseSKESKForceV4Interop ->+ encryptSEIPDv1WithSKESK+ (passphraseEncryptSymmetricAlgorithm request)+ (passphraseEncryptS2K request)+ (passphraseEncryptSEIPDv1IVOverride request)+ (passphraseEncryptPassphrase request)+ (passphraseEncryptPayload request)+ PassphraseSKESKPreferV6 -> do+ let messagePolicy = policyMessageEncryption (policyForRFC RFC9580)+ aead =+ fromMaybe+ (messageDefaultAEADAlgorithm messagePolicy)+ (passphraseEncryptSEIPDv2AEADOverride request)+ chunkSize =+ fromMaybe+ (messageDefaultChunkSize messagePolicy)+ (passphraseEncryptSEIPDv2ChunkSizeOverride request)+ salt <-+ maybe+ (Salt <$> getRandomBytes (messageSEIPDv2SaltOctets messagePolicy))+ pure+ (passphraseEncryptSEIPDv2SaltOverride request)+ pure $+ encryptSEIPDv2WithSKESK+ (passphraseEncryptSymmetricAlgorithm request)+ aead+ chunkSize+ salt+ (passphraseEncryptS2K request)+ (passphraseEncryptPassphrase request)+ (passphraseEncryptPayload request)++defaultRecipientPayloadShape :: RecipientPayloadShape+defaultRecipientPayloadShape =+ RecipientPayloadShape+ { recipientPayloadDataType = BinaryData+ , recipientPayloadFileName = BL.empty+ , recipientPayloadTimestamp = 0+ , recipientPayloadUseOnePassSignatures = False+ , recipientPayloadSignatures = []+ }++data RecipientEncryptResult+ = RecipientEncryptResult+ { recipientEncryptPackets :: [Pkt]+ , recipientEncryptSessionMaterial :: PKESKSessionMaterial+ }+ deriving (Eq, Show)++data RecipientEncryptRequestOverrides (v :: SEIPDVersion) where+ RecipientEncryptRequestSEIPDv1Overrides+ :: { recipientEncryptRequestIVOverride :: Maybe IV+ }+ -> RecipientEncryptRequestOverrides 'SEIPDv1+ {- | For SEIPDv2 requests:+ - when AEAD override is 'Nothing', encrypt-side capability negotiation+ selects a common recipient-supported AEAD algorithm (if enabled).+ - when AEAD override is 'Just', the explicit AEAD wins.+ -}+ RecipientEncryptRequestSEIPDv2Overrides+ :: { recipientEncryptRequestAEADOverride :: Maybe AEADAlgorithm+ , recipientEncryptRequestChunkSizeOverride :: Maybe Word8+ , recipientEncryptRequestSaltOverride :: Maybe Salt+ }+ -> RecipientEncryptRequestOverrides 'SEIPDv2++data RecipientEncryptRequest (v :: SEIPDVersion)+ = RecipientEncryptRequest+ { recipientEncryptRequestTargets :: [RecipientEncryptionTarget]+ -- ^ Recipient encryption targets. At least one target is required.+ , recipientEncryptRequestPayloadShape :: RecipientPayloadShape+ , recipientEncryptRequestPayload :: B.ByteString+ , recipientEncryptRequestSymmetricOverride+ :: Maybe SymmetricAlgorithm+ {- ^ Explicit symmetric algorithm override. When 'Nothing', the selected+ mode (negotiated or legacy) determines algorithm selection.+ -}+ , recipientEncryptRequestOverrides+ :: RecipientEncryptRequestOverrides v+ }++deriving instance Eq (RecipientEncryptRequestOverrides v)+deriving instance Show (RecipientEncryptRequestOverrides v)+deriving instance Eq (RecipientEncryptRequest v)+deriving instance Show (RecipientEncryptRequest v)++{- | Encode the RFC 9580 PKESK/SKESK session-key material:+ one-octet algorithm ID, raw session key, then 16-bit checksum.+-}+encodeOpenPGPSessionMaterial+ :: SymmetricAlgorithm+ -> SessionKey+ -> Either PKESKEncryptError B.ByteString+encodeOpenPGPSessionMaterial symalgo sessionKey =+ unPKESKV3SessionMaterial+ <$> mkPKESKV3SessionMaterial symalgo sessionKey++-- | Generate a fresh session key and return both raw and encoded forms.+generateSessionKeyMaterial+ :: MonadRandom m+ => SymmetricAlgorithm+ -> m (Either PKESKEncryptError PKESKSessionMaterial)+generateSessionKeyMaterial symalgo =+ case keySize symalgo of+ Left err ->+ pure+ ( Left+ (UnsupportedSessionKeyAlgorithm symalgo (renderCipherError err))+ )+ Right keyLen -> do+ sessionKeyBytes <- getRandomBytes keyLen+ let sessionKey = SessionKey sessionKeyBytes+ pure (mkPKESKSessionMaterial symalgo sessionKey)++canonicalizePKESKRecipientId+ :: PKESKPayload -> Either PKESKEncryptError PKESKPayload+canonicalizePKESKRecipientId payload =+ case payload of+ PKESKPayloadV6Packet payloadV6 ->+ PKESKPayloadV6Packet <$> canonicalizePKESKRecipientIdV6 payloadV6+ _ -> Right payload++canonicalizePKESKRecipientIdV6+ :: PKESKPayloadV6 -> Either PKESKEncryptError PKESKPayloadV6+canonicalizePKESKRecipientIdV6 (PKESKPayloadV6 rid pka esk) =+ (\normalizedRid -> PKESKPayloadV6 normalizedRid pka esk)+ <$> canonicalizeRecipientKeyIdentifier rid++canonicalizeRecipientKeyIdentifier+ :: BL.ByteString -> Either PKESKEncryptError BL.ByteString+canonicalizeRecipientKeyIdentifier rid+ | BL.length rid == 20 || BL.length rid == 32 = Right rid+ | BL.length rid == 21 && BL.head rid == 0x04 =+ Right (BL.tail rid)+ | BL.length rid == 33 && BL.head rid == 0x06 =+ Right (BL.tail rid)+ | otherwise =+ Left+ ( InvalidRecipientIdentifier+ ( "unsupported PKESK recipient identifier length/prefix: "+ ++ show (BL.length rid)+ )+ )++canonicalizePKESKPacketRecipientIds+ :: [Pkt] -> Either PKESKEncryptError [Pkt]+canonicalizePKESKPacketRecipientIds =+ mapM+ ( \pkt ->+ case pkt of+ PKESKPkt payload -> fmap PKESKPkt (canonicalizePKESKRecipientId payload)+ _ -> Right pkt+ )++-- | Build a v6 PKESK payload for one recipient key according to the selected version policy.+buildPKESKPayloadForRecipient+ :: MonadRandom m+ => PKESKVersionPolicy+ -> SomePKPayload+ -> PKESKSessionMaterial+ -> m (Either PKESKEncryptError PKESKPayload)+buildPKESKPayloadForRecipient policy recipient material =+ case policy of+ ForceV3Interop ->+ buildPKESKv3PayloadForRecipient+ recipient+ (pkeskV3SessionMaterial material)+ PreferV6 ->+ case _pkalgo recipient of+ RSA ->+ fmap+ (fmap PKESKPayloadV6Packet)+ (buildRsaPKESKv6 recipient material)+ ECDH ->+ fmap+ (fmap PKESKPayloadV6Packet)+ (buildECDHPKESKv6 recipient material)+ X25519 ->+ fmap+ (fmap PKESKPayloadV6Packet)+ (buildX25519PKESKv6 recipient (pkeskV6RawSessionMaterial material))+ X448 ->+ fmap+ (fmap PKESKPayloadV6Packet)+ (buildX448PKESKv6 recipient (pkeskV6RawSessionMaterial material))+ pka -> pure (Left (UnsupportedRecipientAlgorithm pka))++-- | Build a PKESK packet for one recipient key according to the selected version policy.+buildPKESKPktForRecipient+ :: MonadRandom m+ => PKESKVersionPolicy+ -> SomePKPayload+ -> PKESKSessionMaterial+ -> m (Either PKESKEncryptError Pkt)+buildPKESKPktForRecipient policy recipient material =+ fmap+ (fmap PKESKPkt)+ (buildPKESKPayloadForRecipient policy recipient material)++-- | Build a legacy PKESKv3 payload for v4/v3 RSA recipient interop.+buildPKESKv3PayloadForRecipient+ :: MonadRandom m+ => SomePKPayload+ -> PKESKV3SessionMaterial+ -> m (Either PKESKEncryptError PKESKPayload)+buildPKESKv3PayloadForRecipient recipient material =+ fmap+ (fmap PKESKPayloadV3Packet)+ (buildPKESKv3PayloadForRecipientTyped recipient material)++buildPKESKv3PayloadForRecipientTyped+ :: MonadRandom m+ => SomePKPayload+ -> PKESKV3SessionMaterial+ -> m (Either PKESKEncryptError PKESKPayloadV3)+buildPKESKv3PayloadForRecipientTyped recipient material =+ case _pkalgo recipient of+ RSA -> buildRsaPKESKv3 recipient material+ DeprecatedRSAEncryptOnly -> buildRsaPKESKv3 recipient material+ ECDH -> buildECDHPKESKv3 recipient material+ pka -> pure (Left (UnsupportedRecipientAlgorithm pka))++-- | Build a legacy PKESKv3 packet for v4/v3 RSA recipient interop.+buildPKESKv3PktForRecipient+ :: MonadRandom m+ => SomePKPayload+ -> PKESKV3SessionMaterial+ -> m (Either PKESKEncryptError Pkt)+buildPKESKv3PktForRecipient recipient material =+ fmap+ (fmap PKESKPkt)+ (buildPKESKv3PayloadForRecipient recipient material)++-- | Build PKESK packets for all recipients with a single shared session key.+buildPKESKPktsForRecipientTargetsWithSelector+ :: MonadRandom m+ => ( RecipientEncryptionTarget+ -> Either PKESKEncryptError RecipientPKESKVersionStrategy+ )+ -> [RecipientEncryptionTarget]+ -> PKESKSessionMaterial+ -> m (Either PKESKEncryptError [Pkt])+buildPKESKPktsForRecipientTargetsWithSelector selector targets material =+ buildPKESKPktsForRecipientTargetsWithSelectorTyped+ ( \target ->+ promoteRecipientStrategy <$> selector target+ )+ targets+ material++buildPKESKPktsForRecipientTargetsWithSelectorTyped+ :: MonadRandom m+ => ( RecipientEncryptionTarget+ -> Either PKESKEncryptError SomeRecipientPKESKVersionStrategyW+ )+ -> [RecipientEncryptionTarget]+ -> PKESKSessionMaterial+ -> m (Either PKESKEncryptError [Pkt])+buildPKESKPktsForRecipientTargetsWithSelectorTyped selector targets material+ | null targets = pure (Left NoRecipientsProvided)+ | otherwise =+ case preparePKESKVersionedMaterial material of+ Left err -> pure (Left err)+ Right (v3Material, v6RawMaterial) -> do+ pkeskResults <-+ mapM+ ( \target ->+ case selector target of+ Left err -> pure (Left err)+ Right (SomeRecipientPKESKVersionStrategyW RecipientPreferV6W) ->+ buildPKESKPktForRecipientWithPreparedPayload+ RecipientPreferV6W+ (recipientEncryptionTargetKey target)+ (RecipientPreferV6Payload material v6RawMaterial)+ Right+ (SomeRecipientPKESKVersionStrategyW RecipientForceV3InteropW) ->+ buildPKESKPktForRecipientWithPreparedPayload+ RecipientForceV3InteropW+ (recipientEncryptionTargetKey target)+ (RecipientForceV3Payload v3Material)+ )+ targets+ pure+ (sequence pkeskResults >>= canonicalizePKESKPacketRecipientIds)++preparePKESKVersionedMaterial+ :: PKESKSessionMaterial+ -> Either+ PKESKEncryptError+ (PKESKV3SessionMaterial, PKESKV6RawSessionMaterial)+preparePKESKVersionedMaterial material = do+ v3Material <-+ mkPKESKV3SessionMaterial+ (pkeskSessionAlgorithm material)+ (pkeskSessionKey material)+ v6RawMaterial <-+ mkPKESKV6RawSessionMaterial+ (pkeskSessionAlgorithm material)+ (pkeskSessionKey material)+ pure (v3Material, v6RawMaterial)++data+ RecipientPKESKRequestPayload+ (strategy :: RecipientPKESKVersionStrategy)+ where+ RecipientForceV3Payload+ :: PKESKV3SessionMaterial+ -> RecipientPKESKRequestPayload 'RecipientForceV3Interop+ RecipientPreferV6Payload+ :: PKESKSessionMaterial+ -> PKESKV6RawSessionMaterial+ -> RecipientPKESKRequestPayload 'RecipientPreferV6++buildPKESKPktForRecipientWithPreparedPayload+ :: MonadRandom m+ => RecipientPKESKVersionStrategyW strategy+ -> SomePKPayload+ -> RecipientPKESKRequestPayload strategy+ -> m (Either PKESKEncryptError Pkt)+buildPKESKPktForRecipientWithPreparedPayload strategy recipient payload =+ fmap fmapPKESKPkt payloadResult+ where+ fmapPKESKPkt = fmap PKESKPkt+ payloadResult =+ case (strategy, payload) of+ (RecipientForceV3InteropW, RecipientForceV3Payload v3Material) ->+ buildPKESKv3PayloadForRecipient recipient v3Material+ ( RecipientPreferV6W+ , RecipientPreferV6Payload material v6RawMaterial+ ) ->+ case _pkalgo recipient of+ RSA ->+ fmap+ (fmap PKESKPayloadV6Packet)+ (buildRsaPKESKv6 recipient material)+ ECDH ->+ fmap+ (fmap PKESKPayloadV6Packet)+ (buildECDHPKESKv6 recipient material)+ X25519 ->+ fmap+ (fmap PKESKPayloadV6Packet)+ (buildX25519PKESKv6 recipient v6RawMaterial)+ X448 ->+ fmap+ (fmap PKESKPayloadV6Packet)+ (buildX448PKESKv6 recipient v6RawMaterial)+ pka ->+ pure (Left (UnsupportedRecipientAlgorithm pka))++{- | Encrypt for recipient targets with capability negotiation enabled.++By default this negotiates a common symmetric and (for SEIPDv2) AEAD+algorithm from recipient capabilities when available. Explicit request+overrides still take precedence.+-}+encryptForRecipients+ :: MonadRandom m+ => RecipientEncryptRequest v+ -> m (Either PKESKEncryptError RecipientEncryptResult)+encryptForRecipients =+ encryptForRecipientsWithCapabilityNegotiation+ RecipientCapabilityNegotiationOn++{- | Encrypt for recipient targets without recipient capability negotiation.++This preserves legacy behavior by using policy defaults unless request+overrides are provided.+-}+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+intersection of recipient preferences constrained by the active policy.+When off, policy defaults are used.+-}+encryptForRecipientsWithCapabilityNegotiation+ :: MonadRandom m+ => RecipientCapabilityNegotiationMode+ -> RecipientEncryptRequest v+ -> m (Either PKESKEncryptError RecipientEncryptResult)+encryptForRecipientsWithCapabilityNegotiation negotiationMode request+ | null targets = pure (Left NoRecipientsProvided)+ | otherwise =+ case selectSymmetricAlgorithm+ negotiationMode+ request+ messagePolicy+ targets of+ Left err -> pure (Left err)+ Right symalgo -> do+ sessionMaterialResult <- generateSessionKeyMaterial symalgo+ case sessionMaterialResult of+ Left err -> pure (Left err)+ Right sessionMaterial -> do+ pkeskResult <-+ buildPKESKPktsForRecipientTargetsWithSelectorTyped+ (recipientVersionStrategyForProfileTyped profileW)+ targets+ sessionMaterial+ case pkeskResult of+ Left err -> pure (Left err)+ Right pkeskPkts -> do+ payloadResult <- case recipientEncryptRequestOverrides request of+ RecipientEncryptRequestSEIPDv2Overrides+ { recipientEncryptRequestAEADOverride = aeadOverride+ , recipientEncryptRequestChunkSizeOverride = chunkSizeOverride+ , recipientEncryptRequestSaltOverride = saltOverride+ } ->+ case recipientsMissingSEIPDv2Support targets of+ [] -> do+ case selectAEADAlgorithm+ negotiationMode+ messagePolicy+ targets+ aeadOverride of+ Left err -> pure (Left err)+ Right aead -> do+ salt <- maybe (Salt <$> getRandomBytes 32) pure saltOverride+ let chunkSize =+ maybe+ (messageDefaultChunkSize messagePolicy)+ id+ chunkSizeOverride+ pure $+ buildEncryptedPacketSequenceWithShape+ symalgo+ aead+ chunkSize+ (recipientEncryptRequestPayloadShape request)+ salt+ (pkeskSessionKey sessionMaterial)+ pkeskPkts+ (recipientEncryptRequestPayload request)+ _missingSEIPDv2 ->+ case recipientsMissingSEIPDv1Support targets of+ [] ->+ buildSEIPDv1PayloadWithIV+ symalgo+ sessionMaterial+ pkeskPkts+ Nothing+ missingSEIPDv1 ->+ pure+ ( Left+ ( RecipientCapabilitySelectionFailure+ (RecipientCapabilityMissingSEIPDv1Support missingSEIPDv1)+ )+ )+ RecipientEncryptRequestSEIPDv1Overrides+ { recipientEncryptRequestIVOverride = ivOverride+ } ->+ case recipientsMissingSEIPDv1Support targets of+ [] ->+ buildSEIPDv1PayloadWithIV+ symalgo+ sessionMaterial+ pkeskPkts+ ivOverride+ missingSEIPDv1 ->+ pure+ ( Left+ ( RecipientCapabilitySelectionFailure+ (RecipientCapabilityMissingSEIPDv1Support missingSEIPDv1)+ )+ )+ pure $+ fmap+ ( \pkts ->+ RecipientEncryptResult+ { recipientEncryptPackets = pkts+ , recipientEncryptSessionMaterial = sessionMaterial+ }+ )+ payloadResult+ where+ targets = recipientEncryptRequestTargets request+ profileW =+ profileForPayloadVersionW+ (recipientEncryptRequestOverrides request)+ messagePolicy =+ case profileW of+ EncryptStrictDefaultW ->+ policyMessageEncryption (policyForRFC RFC9580)+ EncryptInteropLegacyW ->+ policyMessageEncryption (policyForRFC RFC4880)++ buildSEIPDv1PayloadWithIV+ :: MonadRandom m+ => SymmetricAlgorithm+ -> PKESKSessionMaterial+ -> [Pkt]+ -> Maybe IV+ -> m (Either PKESKEncryptError [Pkt])+ buildSEIPDv1PayloadWithIV symalgo sessionMaterial pkeskPkts ivOverride = do+ ivResult <-+ case ivOverride of+ Just iv -> pure (Right iv)+ Nothing ->+ let keyBytes = unSessionKey (pkeskSessionKey sessionMaterial)+ in case withSymmetricCipher symalgo keyBytes (\c -> Right (blockSize c)) of+ Left err -> pure (Left (PayloadBuildFailure (renderCipherError err)))+ Right n -> fmap (Right . IV) (getRandomBytes n)+ case ivResult of+ Left err -> pure (Left err)+ Right iv ->+ pure $+ buildEncryptedPacketSequenceWithShapeSEIPDv1+ symalgo+ iv+ (recipientEncryptRequestPayloadShape request)+ (pkeskSessionKey sessionMaterial)+ pkeskPkts+ (recipientEncryptRequestPayload request)++ recipientsMissingSEIPDv1Support+ :: [RecipientEncryptionTarget] -> [SomePKPayload]+ recipientsMissingSEIPDv1Support =+ map recipientEncryptionTargetKey+ . filter (not . targetAdvertisesSEIPDv1Support)++ recipientsMissingSEIPDv2Support+ :: [RecipientEncryptionTarget] -> [SomePKPayload]+ recipientsMissingSEIPDv2Support =+ map recipientEncryptionTargetKey+ . filter (not . targetAdvertisesSEIPDv2Support)++ targetAdvertisesSEIPDv1Support+ :: RecipientEncryptionTarget -> Bool+ targetAdvertisesSEIPDv1Support target =+ case recipientEncryptionTargetCapabilities target of+ Nothing -> True+ Just caps -> recipientCapabilityAdvertisesSEIPDv1Support caps++ targetAdvertisesSEIPDv2Support+ :: RecipientEncryptionTarget -> Bool+ targetAdvertisesSEIPDv2Support target =+ case recipientEncryptionTargetCapabilities target of+ Nothing -> True+ Just caps -> recipientCapabilityAdvertisesSEIPDv2Support caps++selectSymmetricAlgorithm+ :: RecipientCapabilityNegotiationMode+ -> RecipientEncryptRequest v+ -> MessageEncryptionPolicy+ -> [RecipientEncryptionTarget]+ -> Either PKESKEncryptError SymmetricAlgorithm+selectSymmetricAlgorithm negotiationMode request messagePolicy targets =+ case recipientEncryptRequestSymmetricOverride request of+ Just override -> Right override+ Nothing ->+ case negotiationMode of+ RecipientCapabilityNegotiationOff ->+ Right (messageDefaultSymmetricAlgorithm messagePolicy)+ RecipientCapabilityNegotiationOn ->+ negotiateSymmetricAlgorithm messagePolicy targets++selectAEADAlgorithm+ :: RecipientCapabilityNegotiationMode+ -> MessageEncryptionPolicy+ -> [RecipientEncryptionTarget]+ -> Maybe AEADAlgorithm+ -> Either PKESKEncryptError AEADAlgorithm+selectAEADAlgorithm negotiationMode messagePolicy targets override =+ case override of+ Just explicit -> Right explicit+ Nothing ->+ case negotiationMode of+ RecipientCapabilityNegotiationOff ->+ Right (messageDefaultAEADAlgorithm messagePolicy)+ RecipientCapabilityNegotiationOn ->+ negotiateAEADAlgorithm messagePolicy targets++negotiateSymmetricAlgorithm+ :: MessageEncryptionPolicy+ -> [RecipientEncryptionTarget]+ -> Either PKESKEncryptError SymmetricAlgorithm+negotiateSymmetricAlgorithm messagePolicy targets =+ chooseCommonAlgorithm+ policyOrder+ recipientChoices+ ( RecipientCapabilityNoCommonSymmetricAlgorithms+ (concat recipientChoices)+ )+ where+ policyOrder =+ case messageSEIPDv2SymmetricAlgorithms messagePolicy of+ [] -> [messageDefaultSymmetricAlgorithm messagePolicy]+ syms -> syms+ recipientChoices = map choicesForTarget targets+ choicesForTarget target =+ case recipientEncryptionTargetCapabilities target of+ Just caps ->+ let preferred = recipientCapabilityPreferredSymmetricAlgorithms caps+ allowed = [alg | alg <- policyOrder, alg `elem` preferred]+ in if null allowed+ then policyOrder+ else allowed+ Nothing -> policyOrder++negotiateAEADAlgorithm+ :: MessageEncryptionPolicy+ -> [RecipientEncryptionTarget]+ -> Either PKESKEncryptError AEADAlgorithm+negotiateAEADAlgorithm messagePolicy targets =+ chooseCommonAlgorithm+ policyOrder+ recipientChoices+ ( RecipientCapabilityNoCommonAEADAlgorithms+ (concat recipientChoices)+ )+ where+ policyOrder =+ foldl'+ addIfMissing+ []+ (messageDefaultAEADAlgorithm messagePolicy : [OCB, EAX, GCM])+ recipientChoices = map choicesForTarget targets+ choicesForTarget target =+ case recipientEncryptionTargetCapabilities target of+ Just caps ->+ let preferred = recipientCapabilityPreferredAEADAlgorithms caps+ allowed = [alg | alg <- policyOrder, alg `elem` preferred]+ in if null allowed+ then policyOrder+ else allowed+ Nothing -> policyOrder+ addIfMissing acc x+ | x `elem` acc = acc+ | otherwise = acc ++ [x]++chooseCommonAlgorithm+ :: Eq a+ => [a]+ -> [[a]]+ -> RecipientCapabilityError+ -> Either PKESKEncryptError a+chooseCommonAlgorithm policyOrder recipientChoices err =+ case recipientChoices of+ [] -> Left (RecipientCapabilitySelectionFailure err)+ (firstChoices : restChoices) ->+ let common = foldl' intersectOrdered firstChoices restChoices+ orderedCommon = [alg | alg <- policyOrder, alg `elem` common]+ in case orderedCommon of+ (selected : _) -> Right selected+ [] -> Left (RecipientCapabilitySelectionFailure err)+ where+ intersectOrdered as bs = [a | a <- as, a `elem` bs]++promoteRecipientStrategy+ :: RecipientPKESKVersionStrategy+ -> SomeRecipientPKESKVersionStrategyW+promoteRecipientStrategy RecipientPreferV6 =+ SomeRecipientPKESKVersionStrategyW RecipientPreferV6W+promoteRecipientStrategy RecipientForceV3Interop =+ SomeRecipientPKESKVersionStrategyW RecipientForceV3InteropW++demoteRecipientStrategy+ :: RecipientPKESKVersionStrategyW strategy+ -> RecipientPKESKVersionStrategy+demoteRecipientStrategy RecipientPreferV6W = RecipientPreferV6+demoteRecipientStrategy RecipientForceV3InteropW = RecipientForceV3Interop++demoteSomeRecipientStrategy+ :: SomeRecipientPKESKVersionStrategyW+ -> RecipientPKESKVersionStrategy+demoteSomeRecipientStrategy (SomeRecipientPKESKVersionStrategyW strategyW) =+ demoteRecipientStrategy strategyW++promoteEncryptCompatibilityProfile+ :: EncryptCompatibilityProfile+ -> SomeEncryptCompatibilityProfileW+promoteEncryptCompatibilityProfile EncryptStrictDefault =+ SomeEncryptCompatibilityProfileW EncryptStrictDefaultW+promoteEncryptCompatibilityProfile EncryptInteropLegacy =+ SomeEncryptCompatibilityProfileW EncryptInteropLegacyW++{- | High-level encrypt-side helper for public-key recipient encryption.++Returns a complete packet sequence:+@[PKESK ..., SEIPD2 ...]@.+-}+buildEncryptedPacketSequence+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> Word8+ -> RecipientPayloadShape+ -> Salt+ -> SessionKey+ -> [Pkt]+ -> B.ByteString+ -> Either String [Pkt]+buildEncryptedPacketSequence symalgo aead chunkSize payloadShape salt sessionKey pkesks payload =+ first+ renderPKESKEncryptError+ ( buildEncryptedPacketSequenceWithShape+ symalgo+ aead+ chunkSize+ payloadShape+ salt+ sessionKey+ pkesks+ payload+ )++buildEncryptedPacketSequenceWithShape+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> Word8+ -> RecipientPayloadShape+ -> Salt+ -> SessionKey+ -> [Pkt]+ -> B.ByteString+ -> Either PKESKEncryptError [Pkt]+buildEncryptedPacketSequenceWithShape symalgo aead chunkSize payloadShape salt sessionKey pkesks payload = do+ onePassSignatures <-+ first+ (PayloadBuildFailure . renderOPSBuildError)+ (buildOnePassSignaturePackets payloadShape)+ let signatures = recipientPayloadSignatures payloadShape+ literalBlock =+ Block+ ( onePassSignatures+ ++ [ LiteralDataPkt+ (recipientPayloadDataType payloadShape)+ (recipientPayloadFileName payloadShape)+ (recipientPayloadTimestamp payloadShape)+ (BL.fromStrict payload)+ ]+ ++ map SignaturePkt signatures+ )+ ciphertext <-+ first PayloadBuildFailure $+ encryptSEIPDv2Payload+ symalgo+ aead+ chunkSize+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock)))+ Right+ ( pkesks+ ++ [ SymEncIntegrityProtectedDataPkt+ (SEIPD2 symalgo aead chunkSize salt (BL.fromStrict ciphertext))+ ]+ )++-- | Encrypt a plaintext block with OpenPGP CFB + MDC to produce a SEIPDv1 ciphertext.+encryptSEIPDv1Payload+ :: SymmetricAlgorithm+ -> IV+ -> SessionKey+ -> B.ByteString+ -- ^ inner packet block plaintext+ -> Either String B.ByteString+encryptSEIPDv1Payload symalgo iv (SessionKey keyBytes) plaintext =+ let cleartextWithMDC = plaintext <> mdcTrailerForSEIPDv1 iv plaintext+ in first+ renderCipherError+ ( encryptOpenPGPCfbRaw+ OpenPGPCFBNoResyncW+ symalgo+ iv+ cleartextWithMDC+ keyBytes+ )++-- | Build a complete RFC 4880-conformant packet sequence using SEIPDv1 (CFB + MDC).+buildEncryptedPacketSequenceWithShapeSEIPDv1+ :: SymmetricAlgorithm+ -> IV+ -> RecipientPayloadShape+ -> SessionKey+ -> [Pkt]+ -> B.ByteString+ -> Either PKESKEncryptError [Pkt]+buildEncryptedPacketSequenceWithShapeSEIPDv1 symalgo iv payloadShape sessionKey pkesks payload = do+ onePassSignatures <-+ first+ (PayloadBuildFailure . renderOPSBuildError)+ (buildOnePassSignaturePackets payloadShape)+ let signatures = recipientPayloadSignatures payloadShape+ literalBlock =+ Block+ ( onePassSignatures+ ++ [ LiteralDataPkt+ (recipientPayloadDataType payloadShape)+ (recipientPayloadFileName payloadShape)+ (recipientPayloadTimestamp payloadShape)+ (BL.fromStrict payload)+ ]+ ++ map SignaturePkt signatures+ )+ ciphertext <-+ first PayloadBuildFailure $+ encryptSEIPDv1Payload+ symalgo+ iv+ sessionKey+ (BL.toStrict (runPut (put literalBlock)))+ Right+ ( pkesks+ ++ [ SymEncIntegrityProtectedDataPkt+ (SEIPD1 1 (BL.fromStrict ciphertext))+ ]+ )++buildOnePassSignaturePackets+ :: RecipientPayloadShape -> Either OPSBuildError [Pkt]+buildOnePassSignaturePackets payloadShape+ | not (recipientPayloadUseOnePassSignatures payloadShape) =+ Right []+ | null signatures = Right []+ | otherwise =+ fmap (map OnePassSignaturePkt) $+ sequence+ (zipWith buildOnePassSignature nestedFlags (reverse signatures))+ where+ signatures = recipientPayloadSignatures payloadShape+ nestedFlags = replicate (length signatures - 1) True ++ [False]++data OnePassSignatureBuildCase where+ OnePassSignatureBuildCaseV3+ :: SignaturePayloadV 'SigPayloadV3 -> OnePassSignatureBuildCase+ OnePassSignatureBuildCaseV4+ :: SignaturePayloadV 'SigPayloadV4 -> OnePassSignatureBuildCase+ OnePassSignatureBuildCaseV6+ :: SignaturePayloadV 'SigPayloadV6 -> OnePassSignatureBuildCase+ OnePassSignatureBuildCaseOther+ :: PacketVersion -> OnePassSignatureBuildCase++onePassSignatureBuildCase+ :: SignaturePayload -> OnePassSignatureBuildCase+onePassSignatureBuildCase sig =+ case toSomeSignaturePayload sig of+ SomeSignaturePayload (payload@SigPayloadV3Data {}) ->+ OnePassSignatureBuildCaseV3 payload+ SomeSignaturePayload (payload@SigPayloadV4Data {}) ->+ OnePassSignatureBuildCaseV4 payload+ SomeSignaturePayload (payload@SigPayloadV6Data {}) ->+ OnePassSignatureBuildCaseV6 payload+ SomeSignaturePayload (SigPayloadOtherData version _) ->+ OnePassSignatureBuildCaseOther version++buildOnePassSignature+ :: NestedFlag+ -> SignaturePayload+ -> Either OPSBuildError OnePassSignaturePayload+buildOnePassSignature nestedFlag sig =+ case onePassSignatureBuildCase sig of+ OnePassSignatureBuildCaseV3+ (SigPayloadV3Data sigType _ issuerKeyId pubkeyAlgo hashAlgo _ _) ->+ Right+ ( OPSPayloadV3Packet+ (OPSPayloadV3 3 sigType hashAlgo pubkeyAlgo issuerKeyId nestedFlag)+ )+ OnePassSignatureBuildCaseV4+ ( SigPayloadV4Data+ sigType+ pubkeyAlgo+ hashAlgo+ hashedSubpackets+ unhashedSubpackets+ _+ _+ ) ->+ case signatureIssuerKeyId hashedSubpackets unhashedSubpackets of+ Just issuerKeyId ->+ Right+ ( OPSPayloadV3Packet+ (OPSPayloadV3 3 sigType hashAlgo pubkeyAlgo issuerKeyId nestedFlag)+ )+ Nothing ->+ Left OPSBuildMissingIssuerKeyId+ OnePassSignatureBuildCaseV6+ ( SigPayloadV6Data+ sigType+ pubkeyAlgo+ hashAlgo+ salt+ hashedSubpackets+ unhashedSubpackets+ _+ _+ ) ->+ case signatureIssuerFingerprint+ ( BTypes.issuerFingerprintVersionToPacketVersion+ BTypes.IssuerFingerprintV6+ )+ hashedSubpackets+ unhashedSubpackets of+ Just signerFingerprint+ | BL.length signerFingerprint == 32 ->+ Right+ ( OPSPayloadV6Packet+ ( OPSPayloadV6+ sigType+ hashAlgo+ pubkeyAlgo+ salt+ signerFingerprint+ nestedFlag+ )+ )+ | otherwise ->+ Left+ (OPSBuildFingerprintWrongLength (BL.length signerFingerprint))+ Nothing ->+ Left OPSBuildMissingIssuerFingerprint+ OnePassSignatureBuildCaseOther version ->+ Left (OPSBuildUnsupportedSigVersion version)++signatureIssuerKeyId+ :: [SigSubPacket] -> [SigSubPacket] -> Maybe EightOctetKeyId+signatureIssuerKeyId hashedSubpackets unhashedSubpackets =+ case findIssuerKeyId hashedSubpackets of+ Just issuerKeyId -> Just issuerKeyId+ Nothing ->+ case findIssuerKeyId unhashedSubpackets of+ Just issuerKeyId -> Just issuerKeyId+ Nothing ->+ case signatureIssuerFingerprint+ ( BTypes.issuerFingerprintVersionToPacketVersion+ BTypes.IssuerFingerprintV4+ )+ hashedSubpackets+ unhashedSubpackets of+ Just issuerFingerprintBytes ->+ if BL.length issuerFingerprintBytes >= 8+ then+ Just+ ( EightOctetKeyId+ ( BL.drop+ (BL.length issuerFingerprintBytes - 8)+ issuerFingerprintBytes+ )+ )+ else Nothing+ Nothing -> Nothing++signatureIssuerFingerprint+ :: PacketVersion+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> Maybe BL.ByteString+signatureIssuerFingerprint expectedVersion hashedSubpackets unhashedSubpackets =+ unFingerprint+ <$> findIssuerFingerprint+ expectedVersion+ hashedSubpackets+ unhashedSubpackets++findIssuerKeyId :: [SigSubPacket] -> Maybe EightOctetKeyId+findIssuerKeyId subpackets =+ case find isIssuerKeyIdSubpacket subpackets of+ Just (SigSubPacket _ (Issuer issuerKeyId)) -> Just issuerKeyId+ _ -> Nothing++findIssuerFingerprint+ :: PacketVersion+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> Maybe Fingerprint+findIssuerFingerprint expectedVersion hashedSubpackets unhashedSubpackets =+ case findIssuerFingerprintIn expectedVersion hashedSubpackets of+ Just issuerFingerprint -> Just issuerFingerprint+ Nothing -> findIssuerFingerprintIn expectedVersion unhashedSubpackets++findIssuerFingerprintIn+ :: PacketVersion -> [SigSubPacket] -> Maybe Fingerprint+findIssuerFingerprintIn expectedVersion subpackets =+ case find (isIssuerFingerprintSubpacket expectedVersion) subpackets of+ Just (SigSubPacket _ (IssuerFingerprint _ issuerFingerprint)) -> Just issuerFingerprint+ _ -> Nothing++isIssuerKeyIdSubpacket :: SigSubPacket -> Bool+isIssuerKeyIdSubpacket (SigSubPacket _ (Issuer _)) = True+isIssuerKeyIdSubpacket _ = False++isIssuerFingerprintSubpacket+ :: PacketVersion -> SigSubPacket -> Bool+isIssuerFingerprintSubpacket expectedVersion (SigSubPacket _ (IssuerFingerprint version _)) =+ BTypes.issuerFingerprintVersionToPacketVersion version+ == expectedVersion+isIssuerFingerprintSubpacket _ _ = False++buildRsaPKESKv6+ :: MonadRandom m+ => SomePKPayload+ -> PKESKSessionMaterial+ -> m (Either PKESKEncryptError PKESKPayloadV6)+buildRsaPKESKv6 recipient material =+ case _pubkey recipient of+ RSAPubKey (RSA_PublicKey publicKey) -> do+ encrypted <-+ RSA15.encrypt publicKey (pkeskEncodedSessionMaterial material)+ pure $+ fmap+ ( \esk ->+ let mpiEsk = runPut (put (MPI (os2ip esk)))+ in PKESKPayloadV6 (recipientKeyIdentifier recipient) RSA mpiEsk+ )+ (first (RecipientKeyWrapFailure RSA . show) encrypted)+ _ ->+ pure+ ( Left+ ( InvalidRecipientKeyMaterial+ RSA+ "recipient PKPayload does not contain an RSA public key"+ )+ )++buildRsaPKESKv3+ :: MonadRandom m+ => SomePKPayload+ -> PKESKV3SessionMaterial+ -> m (Either PKESKEncryptError PKESKPayloadV3)+buildRsaPKESKv3 recipient material =+ case _pubkey recipient of+ RSAPubKey (RSA_PublicKey publicKey) ->+ case eightOctetKeyID recipient of+ Left err ->+ pure+ ( Left+ ( InvalidRecipientKeyMaterial+ (_pkalgo recipient)+ ("failed to derive PKESKv3 recipient key ID: " ++ err)+ )+ )+ Right eoki -> do+ encrypted <-+ RSA15.encrypt publicKey (unPKESKV3SessionMaterial material)+ pure $+ fmap+ ( \esk ->+ PKESKPayloadV3+ 3+ eoki+ (_pkalgo recipient)+ (MPI (os2ip esk) :| [])+ )+ ( first+ (RecipientKeyWrapFailure (_pkalgo recipient) . show)+ encrypted+ )+ _ ->+ pure+ ( Left+ ( InvalidRecipientKeyMaterial+ (_pkalgo recipient)+ "recipient PKPayload does not contain an RSA public key"+ )+ )++buildECDHPKESKv3+ :: MonadRandom m+ => SomePKPayload+ -> PKESKV3SessionMaterial+ -> m (Either PKESKEncryptError PKESKPayloadV3)+buildECDHPKESKv3 recipient material =+ case _pubkey recipient of+ ECDHPubKey ecdhPub kdfHA kdfSA ->+ case eightOctetKeyID recipient of+ Left err ->+ pure+ ( Left+ ( InvalidRecipientKeyMaterial+ ECDH+ ("failed to derive PKESKv3 recipient key ID: " ++ err)+ )+ )+ Right eoki ->+ case ecdhPub of+ ECDSAPubKey (ECDSA_PublicKey recipientPub) -> do+ (ephemeralPub, ephemeralPriv) <-+ ECCGen.generate (ECDSA.public_curve recipientPub)+ case point2MBS (ECDSA.public_q ephemeralPub) of+ Nothing ->+ pure+ ( Left+ ( InvalidRecipientKeyMaterial+ ECDH+ "failed to serialize ECDH ephemeral point"+ )+ )+ Just ephemeralBytes ->+ pure $+ buildEcdhV3Payload+ recipient+ eoki+ ECDH+ ecdhPub+ kdfHA+ kdfSA+ ephemeralBytes+ ( BA.convert+ ( ECCDH.getShared+ (ECDSA.public_curve recipientPub)+ (ECDSA.private_d ephemeralPriv)+ (ECDSA.public_q recipientPub)+ )+ :: B.ByteString+ )+ material+ EdDSAPubKey EdSigningCurve25519 recipientPoint -> do+ ephSecretRaw <- getRandomBytes 32+ pure $+ do+ recipientPublicBytes <-+ normalizeX25519Public (edPointBytes recipientPoint)+ ephSecret <-+ first (RecipientKeyWrapFailure ECDH . show)+ . CE.eitherCryptoError+ $ C25519.secretKey (leftPadTo 32 ephSecretRaw)+ recipientPub <-+ first (RecipientKeyWrapFailure ECDH . show)+ . CE.eitherCryptoError+ $ C25519.publicKey recipientPublicBytes+ let ephPublicBytes =+ B.cons+ 0x40+ (BA.convert (C25519.toPublic ephSecret) :: B.ByteString)+ sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString+ buildEcdhV3Payload+ recipient+ eoki+ ECDH+ ecdhPub+ kdfHA+ kdfSA+ ephPublicBytes+ sharedSecret+ material+ _ ->+ pure+ ( Left+ ( InvalidRecipientKeyMaterial+ ECDH+ "recipient ECDH public key is not RFC6637-compatible"+ )+ )+ _ ->+ pure+ ( Left+ ( InvalidRecipientKeyMaterial+ ECDH+ "recipient PKPayload does not contain ECDH public key material"+ )+ )++buildECDHPKESKv6+ :: MonadRandom m+ => SomePKPayload+ -> PKESKSessionMaterial+ -> m (Either PKESKEncryptError PKESKPayloadV6)+buildECDHPKESKv6 recipient material =+ case _pubkey recipient of+ ECDHPubKey ecdhPub kdfHA kdfSA ->+ case ecdhPub of+ ECDSAPubKey (ECDSA_PublicKey recipientPub) -> do+ (ephemeralPub, ephemeralPriv) <-+ ECCGen.generate (ECDSA.public_curve recipientPub)+ case point2MBS (ECDSA.public_q ephemeralPub) of+ Nothing ->+ pure+ ( Left+ ( InvalidRecipientKeyMaterial+ ECDH+ "failed to serialize ECDH ephemeral point"+ )+ )+ Just ephemeralBytes ->+ pure $+ buildEcdhV6Esk+ recipient+ ECDH+ ecdhPub+ kdfHA+ kdfSA+ ephemeralBytes+ ( BA.convert+ ( ECCDH.getShared+ (ECDSA.public_curve recipientPub)+ (ECDSA.private_d ephemeralPriv)+ (ECDSA.public_q recipientPub)+ )+ :: B.ByteString+ )+ material+ EdDSAPubKey EdSigningCurve25519 recipientPoint -> do+ ephSecretRaw <- getRandomBytes 32+ pure $+ do+ recipientPublicBytes <-+ normalizeX25519Public (edPointBytes recipientPoint)+ ephSecret <-+ first (RecipientKeyWrapFailure ECDH . show)+ . CE.eitherCryptoError+ $ C25519.secretKey (leftPadTo 32 ephSecretRaw)+ recipientPub <-+ first (RecipientKeyWrapFailure ECDH . show)+ . CE.eitherCryptoError+ $ C25519.publicKey recipientPublicBytes+ let ephPublicBytes = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString+ buildEcdhV6Esk+ recipient+ ECDH+ ecdhPub+ kdfHA+ kdfSA+ ephPublicBytes+ sharedSecret+ material+ EdDSAPubKey EdSigningCurve448 recipientPoint -> do+ ephSecretRaw <- getRandomBytes 56+ pure $+ do+ recipientPublicBytes <-+ normalizeX448Public (edPointBytes recipientPoint)+ ephSecret <-+ first (RecipientKeyWrapFailure ECDH . show)+ . CE.eitherCryptoError+ $ C448.secretKey (leftPadTo 56 ephSecretRaw)+ recipientPub <-+ first (RecipientKeyWrapFailure ECDH . show)+ . CE.eitherCryptoError+ $ C448.publicKey recipientPublicBytes+ let ephPublicBytes = BA.convert (C448.toPublic ephSecret) :: B.ByteString+ sharedSecret = BA.convert (C448.dh recipientPub ephSecret) :: B.ByteString+ buildEcdhV6Esk+ recipient+ ECDH+ ecdhPub+ kdfHA+ kdfSA+ ephPublicBytes+ sharedSecret+ material+ _ ->+ pure+ ( Left+ ( InvalidRecipientKeyMaterial+ ECDH+ "recipient ECDH public key is not ECDSA/X25519/X448-compatible"+ )+ )+ _ ->+ pure+ ( Left+ ( InvalidRecipientKeyMaterial+ ECDH+ "recipient PKPayload does not contain ECDH public key material"+ )+ )++buildX25519PKESKv6+ :: MonadRandom m+ => SomePKPayload+ -> PKESKV6RawSessionMaterial+ -> m (Either PKESKEncryptError PKESKPayloadV6)+buildX25519PKESKv6 recipient material = do+ ephSecretRaw <- getRandomBytes 32+ pure $+ do+ recipientPublic <- extractX25519RecipientPublic recipient+ ephSecret <-+ first (RecipientKeyWrapFailure X25519 . show)+ . CE.eitherCryptoError+ $ C25519.secretKey (leftPadTo 32 ephSecretRaw)+ recipientPub <-+ first (RecipientKeyWrapFailure X25519 . show)+ . CE.eitherCryptoError+ $ C25519.publicKey recipientPublic+ let ephPublicBytes = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString+ kek = deriveX25519Kek ephPublicBytes recipientPublic sharedSecret+ wrapped <-+ first (RecipientKeyWrapFailure X25519)+ . aesKeyWrapRFC3394 AES128 kek+ $ unPKESKV6RawSessionMaterial material+ esk <- encodeV6X25519Esk ephPublicBytes wrapped+ Right+ ( PKESKPayloadV6+ (recipientKeyIdentifier recipient)+ X25519+ (BL.fromStrict esk)+ )++buildX448PKESKv6+ :: MonadRandom m+ => SomePKPayload+ -> PKESKV6RawSessionMaterial+ -> m (Either PKESKEncryptError PKESKPayloadV6)+buildX448PKESKv6 recipient material = do+ ephSecretRaw <- getRandomBytes 56+ pure $+ do+ recipientPublic <- extractX448RecipientPublic recipient+ ephSecret <-+ first (RecipientKeyWrapFailure X448 . show)+ . CE.eitherCryptoError+ $ C448.secretKey (leftPadTo 56 ephSecretRaw)+ recipientPub <-+ first (RecipientKeyWrapFailure X448 . show)+ . CE.eitherCryptoError+ $ C448.publicKey recipientPublic+ let ephPublicBytes = BA.convert (C448.toPublic ephSecret) :: B.ByteString+ sharedSecret = BA.convert (C448.dh recipientPub ephSecret) :: B.ByteString+ kek = deriveX448Kek ephPublicBytes recipientPublic sharedSecret+ wrapped <-+ first (RecipientKeyWrapFailure X448)+ . aesKeyWrapRFC3394 AES256 kek+ $ unPKESKV6RawSessionMaterial material+ esk <- encodeV6X448Esk ephPublicBytes wrapped+ Right+ ( PKESKPayloadV6+ (recipientKeyIdentifier recipient)+ X448+ (BL.fromStrict esk)+ )++buildEcdhV6Esk+ :: SomePKPayload+ -> PubKeyAlgorithm+ -> PKey+ -> HashAlgorithm+ -> SymmetricAlgorithm+ -> B.ByteString+ -> B.ByteString+ -> PKESKSessionMaterial+ -> Either PKESKEncryptError PKESKPayloadV6+buildEcdhV6Esk recipient pka ecdhPub kdfHA kdfSA ephemeralBytes sharedSecret material = do+ kdfParam <-+ first (RecipientKdfFailure pka) $+ buildECDHKDFParam recipient pka ecdhPub kdfHA kdfSA+ kek <-+ first (RecipientKdfFailure pka) $+ deriveECDHKek kdfHA kdfSA sharedSecret kdfParam+ wrapped <-+ first (RecipientKeyWrapFailure pka)+ . aesKeyWrapRFC3394 kdfSA kek+ $ padToMultipleOf8 (pkeskEncodedSessionMaterial material)+ esk <- encodeV6EcdhEsk ephemeralBytes wrapped+ Right+ ( PKESKPayloadV6+ (recipientKeyIdentifier recipient)+ pka+ (BL.fromStrict esk)+ )++buildEcdhV3Payload+ :: SomePKPayload+ -> EightOctetKeyId+ -> PubKeyAlgorithm+ -> PKey+ -> HashAlgorithm+ -> SymmetricAlgorithm+ -> B.ByteString+ -> B.ByteString+ -> PKESKV3SessionMaterial+ -> Either PKESKEncryptError PKESKPayloadV3+buildEcdhV3Payload recipient eoki pka ecdhPub kdfHA kdfSA ephemeralBytes sharedSecret material = do+ kdfParam <-+ first (RecipientKdfFailure pka) $+ buildECDHKDFParam recipient pka ecdhPub kdfHA kdfSA+ kek <-+ first (RecipientKdfFailure pka) $+ deriveECDHKek kdfHA kdfSA sharedSecret kdfParam+ wrapped <-+ first (RecipientKeyWrapFailure pka)+ . aesKeyWrapRFC3394 kdfSA kek+ $ padToMultipleOf8 (unPKESKV3SessionMaterial material)+ Right+ ( PKESKPayloadV3+ 3+ eoki+ pka+ (MPI (os2ip ephemeralBytes) :| [MPI (os2ip wrapped)])+ )++recipientKeyIdentifier :: SomePKPayload -> BL.ByteString+recipientKeyIdentifier = unFingerprint . fingerprint++encodeV6EcdhEsk+ :: B.ByteString+ -> B.ByteString+ -> Either PKESKEncryptError B.ByteString+encodeV6EcdhEsk ephemeral wrapped = do+ let ephLen = B.length ephemeral+ if ephLen > 255+ then+ Left+ ( RecipientKeyWrapFailure+ ECDH+ "ephemeral key encoding is too large"+ )+ else+ Right (B.singleton (fromIntegral ephLen) <> ephemeral <> wrapped)++encodeV6X25519Esk+ :: B.ByteString+ -> B.ByteString+ -> Either PKESKEncryptError B.ByteString+encodeV6X25519Esk ephemeral wrapped+ | B.length ephemeral /= 32 =+ Left+ ( RecipientKeyWrapFailure+ X25519+ "X25519 ephemeral key must be exactly 32 octets"+ )+ | B.length wrapped > 255 =+ Left+ ( RecipientKeyWrapFailure+ X25519+ "wrapped session key encoding is too large"+ )+ | otherwise =+ Right+ ( ephemeral+ <> B.singleton (fromIntegral (B.length wrapped))+ <> wrapped+ )++encodeV6X448Esk+ :: B.ByteString+ -> B.ByteString+ -> Either PKESKEncryptError B.ByteString+encodeV6X448Esk ephemeral wrapped+ | B.length ephemeral /= 56 =+ Left+ ( RecipientKeyWrapFailure+ X448+ "X448 ephemeral key must be exactly 56 octets"+ )+ | B.length wrapped > 255 =+ Left+ ( RecipientKeyWrapFailure+ X448+ "wrapped session key encoding is too large"+ )+ | otherwise =+ Right+ ( ephemeral+ <> B.singleton (fromIntegral (B.length wrapped))+ <> wrapped+ )++extractX25519RecipientPublic+ :: SomePKPayload -> Either PKESKEncryptError B.ByteString+extractX25519RecipientPublic recipient =+ case _pubkey recipient of+ EdDSAPubKey EdSigningCurve25519 point ->+ normalizeX25519Public (edPointBytes point)+ ECDHPubKey (EdDSAPubKey EdSigningCurve25519 point) _ _ ->+ normalizeX25519Public (edPointBytes point)+ other ->+ Left+ ( InvalidRecipientKeyMaterial+ X25519+ ("expected X25519-compatible recipient key, got " ++ show other)+ )++extractX448RecipientPublic+ :: SomePKPayload -> Either PKESKEncryptError B.ByteString+extractX448RecipientPublic recipient =+ case _pubkey recipient of+ EdDSAPubKey EdSigningCurve448 point ->+ normalizeX448Public (edPointBytes point)+ ECDHPubKey (EdDSAPubKey EdSigningCurve448 point) _ _ ->+ normalizeX448Public (edPointBytes point)+ other ->+ Left+ ( InvalidRecipientKeyMaterial+ X448+ ("expected X448-compatible recipient key, got " ++ show other)+ )++normalizeX25519Public+ :: B.ByteString -> Either PKESKEncryptError B.ByteString+normalizeX25519Public =+ first (InvalidRecipientKeyMaterial X25519)+ . normalizeMontgomeryPublic+ 32+ "invalid X25519 public key length/prefix: "++normalizeX448Public+ :: B.ByteString -> Either PKESKEncryptError B.ByteString+normalizeX448Public =+ first (InvalidRecipientKeyMaterial X448)+ . normalizeMontgomeryPublic+ 56+ "invalid X448 public key length/prefix: "++edPointBytes :: EdPoint -> B.ByteString+edPointBytes (PrefixedNativeEPoint (EPoint x)) = i2osp x+edPointBytes (NativeEPoint (EPoint x)) = i2osp x++deriveX25519Kek+ :: B.ByteString -> B.ByteString -> B.ByteString -> B.ByteString+deriveX25519Kek ephemeralPublic recipientPublic sharedSecret =+ let ikm = ephemeralPublic <> recipientPublic <> sharedSecret+ prk = extract @CHAlg.SHA256 B.empty ikm+ info = "OpenPGP X25519" :: B.ByteString+ in expand @CHAlg.SHA256 prk info 16++deriveX448Kek+ :: B.ByteString -> B.ByteString -> B.ByteString -> B.ByteString+deriveX448Kek ephemeralPublic recipientPublic sharedSecret =+ let ikm = ephemeralPublic <> recipientPublic <> sharedSecret+ prk = extract @CHAlg.SHA512 B.empty ikm+ info = "OpenPGP X448" :: B.ByteString+ in expand @CHAlg.SHA512 prk info 32++padToMultipleOf8 :: B.ByteString -> B.ByteString+padToMultipleOf8 bs+ | padLen == 0 = bs+ | otherwise = bs <> B.replicate padLen (fromIntegral padLen)+ where+ rem8 = B.length bs `mod` 8+ padLen = if rem8 == 0 then 0 else 8 - rem8++checksum16 :: B.ByteString -> Word16+checksum16 =+ fromIntegral+ . B.foldl'+ (\acc octet -> (acc + fromIntegral octet) `mod` (65536 :: Integer))+ 0++checksum16Bytes :: B.ByteString -> B.ByteString+checksum16Bytes bs =+ B.pack+ [ fromIntegral ((chk `shiftR` 8) .&. 0xff)+ , fromIntegral (chk .&. 0xff)+ ]+ where+ chk = checksum16 bs++aesKeyWrapRFC3394+ :: SymmetricAlgorithm+ -> B.ByteString+ -> B.ByteString+ -> Either String B.ByteString+aesKeyWrapRFC3394 sa kek plain =+ withAESCipher+ "ECDH PKESK currently supports AES KEK algorithms only"+ sa+ kek+ wrapWithCipher+ where+ wrapWithCipher+ :: CCT.BlockCipher cipher => cipher -> Either String B.ByteString+ wrapWithCipher cipher = do+ if B.length plain < 16 || B.length plain `mod` 8 /= 0+ then+ Left+ "ECDH key wrap input must be at least 16 octets and a multiple of 8"+ else Right ()+ let rs = chunksOf8 plain+ if length rs < 2+ then+ Left+ "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+ => cipher+ -> B.ByteString+ -> [B.ByteString]+ -> Either String (B.ByteString, [B.ByteString])+ wrapRounds cipher aInit rsInit = goJ 0 aInit rsInit+ where+ n = length rsInit+ goJ j a rs+ | j > 5 = Right (a, rs)+ | otherwise = do+ (a', rs') <- goI 1 a rs+ goJ (j + 1) a' rs'+ where+ goI i curA curRs+ | i > n = Right (curA, curRs)+ | 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+ aNext = xorBS msb (encodeWord64be t)+ rsNext = (ix (i - 1) .~ lsb) curRs+ goI (i + 1) aNext rsNext++chunksOf8 :: B.ByteString -> [B.ByteString]+chunksOf8 bs+ | B.null bs = []+ | otherwise =+ let (h, t) = B.splitAt 8 bs+ in h : chunksOf8 t++xorBS :: B.ByteString -> B.ByteString -> B.ByteString+xorBS a b = B.pack (B.zipWith xor a b)++encryptSEIPDv1WithSKESK+ :: MonadRandom m+ => SymmetricAlgorithm+ -> S2K+ -> Maybe IV+ -> BL.ByteString+ -> B.ByteString+ -> m (Either String [Pkt])+encryptSEIPDv1WithSKESK symalgo s2k ivOverride passphrase literalPayload = do+ let eSessionKey = do+ keyLen <- symKeySize symalgo+ first renderS2KError (string2Key s2k keyLen passphrase)+ case eSessionKey of+ Left err -> pure (Left err)+ Right sessionKeyMaterial ->+ case first+ renderCipherError+ (withSymmetricCipher symalgo sessionKeyMaterial (pure . blockSize)) of+ Left err -> pure (Left err)+ Right ivLength -> do+ ivBytes <-+ maybe+ (getRandomBytes ivLength)+ (pure . unIV)+ ivOverride+ let iv = IV ivBytes+ let sessionKey = SessionKey sessionKeyMaterial+ case encryptSEIPDv1Payload symalgo iv sessionKey literalPayload of+ Left err -> pure (Left err)+ Right encrypted ->+ pure+ ( Right+ [ SKESKPkt+ ( SKESKPayloadV4Packet+ (SKESKPayloadV4 symalgo s2k Nothing)+ )+ , SymEncIntegrityProtectedDataPkt+ (SEIPD1 1 (BL.fromStrict encrypted))+ ]+ )++encryptSEIPDv2WithSKESK+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> Word8+ -> Salt+ -> S2K+ -> BL.ByteString+ -> B.ByteString+ -> Either String [Pkt]+encryptSEIPDv2WithSKESK symalgo aead chunkSize salt s2k passphrase literalPayload = do+ keyLen <- symKeySize symalgo+ sessionKeyMaterial <-+ first renderS2KError (string2Key s2k keyLen passphrase)+ (_, nonceSize) <-+ aeadModeAndNonceSizeForSEIPDv2+ "unsupported AEAD algorithm for SKESK v6 encrypt"+ aead+ when (B.length (unSalt salt) < nonceSize) $+ Left "SEIPD v2 salt is too short to derive the SKESK v6 IV"+ let skeskIV = B.take nonceSize (unSalt salt)+ kek <- deriveSKESK6KEK symalgo aead sessionKeyMaterial+ (wrappedSessionKey, skeskTag) <-+ encryptSKESK6SessionKey+ symalgo+ aead+ kek+ skeskIV+ sessionKeyMaterial+ let sessionKey = SessionKey sessionKeyMaterial+ encrypted <-+ encryptSEIPDv2Payload+ symalgo+ aead+ chunkSize+ salt+ sessionKey+ literalPayload+ return+ [ SKESKPkt+ ( SKESKPayloadV6Packet+ ( SKESKPayloadV6+ symalgo+ aead+ s2k+ (BL.fromStrict skeskIV)+ (BL.fromStrict wrappedSessionKey)+ (BL.fromStrict skeskTag)+ )+ )+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 symalgo aead chunkSize salt (BL.fromStrict encrypted))+ ]++encryptSEIPDv2WithSKESKBlock+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> Word8+ -> Salt+ -> S2K+ -> BL.ByteString+ -> Block Pkt+ -> Either String [Pkt]+encryptSEIPDv2WithSKESKBlock symalgo aead chunkSize salt s2k passphrase packetBlock =+ encryptSEIPDv2WithSKESK+ symalgo+ aead+ chunkSize+ salt+ s2k+ passphrase+ (BL.toStrict (runPut (put packetBlock)))++encryptSEIPDv2LiteralDataWithSKESK+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> Word8+ -> Salt+ -> S2K+ -> BL.ByteString+ -> B.ByteString+ -> Either String [Pkt]+encryptSEIPDv2LiteralDataWithSKESK symalgo aead chunkSize salt s2k passphrase payload =+ encryptSEIPDv2WithSKESKBlock+ symalgo+ aead+ chunkSize+ salt+ s2k+ passphrase+ ( Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ (BL.fromStrict payload)+ ]+ )++encryptSEIPDv2Payload+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> Word8+ -> Salt+ -> SessionKey+ -> B.ByteString+ -> Either String B.ByteString+encryptSEIPDv2Payload symalgo aead chunkSize salt (SessionKey sessionKey) plaintext = do+ (mode, nonceSize) <- aeadModeAndNonceSize aead+ keyLen <- symKeySize symalgo+ let outputLen = keyLen + nonceSize - 8+ info = B.pack [0xd2, 2, fromFVal symalgo, fromFVal aead, chunkSize]+ prk = extract @CHAlg.SHA256 (unSalt salt) sessionKey+ okm = expand @CHAlg.SHA256 prk info outputLen :: B.ByteString+ messageKey = B.take keyLen okm+ noncePrefix = B.take (nonceSize - 8) (B.drop keyLen okm)+ withAESCipher+ "SEIPD v2 encrypt currently supports AES-128/192/256 only"+ symalgo+ messageKey+ (encryptChunks mode info chunkSize noncePrefix plaintext)++encryptChunks+ :: CCT.BlockCipher cipher+ => CCT.AEADMode+ -> B.ByteString+ -> Word8+ -> B.ByteString+ -> B.ByteString+ -> cipher+ -> Either String B.ByteString+encryptChunks mode info chunkSize noncePrefix plaintext cipher = go 0 plaintext [] 0+ where+ chunkLen = 1 `shiftL` (fromIntegral chunkSize + 6)+ go idx remaining acc totalPlain+ | B.null remaining = do+ (finalTag, finalCipher) <-+ if mode == CCT.AEAD_OCB+ then+ encryptWithOCBRFC7253+ cipher+ (noncePrefix <> encodeWord64be idx)+ (info <> encodeWord64be (fromIntegral totalPlain))+ B.empty+ else do+ aead <- initAEAD idx+ let (tag, out) =+ CCT.aeadSimpleEncrypt+ aead+ (info <> encodeWord64be (fromIntegral totalPlain))+ B.empty+ 16+ Right (tag, out)+ if B.null finalCipher+ then return (B.concat (reverse acc) <> authTagToBS finalTag)+ else Left "expected empty ciphertext for final SEIPD v2 tag"+ | otherwise = do+ let (chunkPlain, rest) = B.splitAt chunkLen remaining+ (tag, chunkCipher) <-+ if mode == CCT.AEAD_OCB+ then+ encryptWithOCBRFC7253+ cipher+ (noncePrefix <> encodeWord64be idx)+ info+ chunkPlain+ else do+ aead <- initAEAD idx+ pure (CCT.aeadSimpleEncrypt aead info chunkPlain 16)+ let chunkOut = chunkCipher <> authTagToBS tag+ go+ (idx + 1)+ rest+ (chunkOut : acc)+ (totalPlain + B.length chunkPlain)++ initAEAD idx =+ first show . CE.eitherCryptoError $+ CCT.aeadInit mode cipher (noncePrefix <> encodeWord64be idx)++aeadModeAndNonceSize+ :: AEADAlgorithm -> Either String (CCT.AEADMode, Int)+aeadModeAndNonceSize =+ aeadModeAndNonceSizeForSEIPDv2+ "unsupported AEAD algorithm for SEIPD v2 encrypt"++symKeySize :: SymmetricAlgorithm -> Either String Int+symKeySize =+ seipdv2SymmetricKeySize+ "unsupported symmetric algorithm for SEIPD v2 encrypt"++authTagToBS :: CCT.AuthTag -> B.ByteString+authTagToBS = BA.convert . CCT.unAuthTag++encodeWord64be :: Word64 -> B.ByteString+encodeWord64be = BL.toStrict . runPut . putWord64be++{- | Compose a complete AEAD-encrypted message with optional literal data and signature.+Returns a packet list (SKESK, SEIPD v2, optional signature) ready for serialization.++Example: @composeMessageWithSEIPDv2 AES256 OCB 6 (Salt 32 bytes)+ (SimpleS2K SHA256) passphrase payload Nothing@+returns @[SKESK v6, SEIPD v2, <ciphertext>]@++If the signature is provided, it will be included in the encrypted payload.+-}+composeMessageWithSEIPDv2+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> Word8+ -> Salt+ -> S2K+ -> BL.ByteString+ -> B.ByteString+ -> Maybe [Pkt]+ -> Either String [Pkt]+composeMessageWithSEIPDv2 symalgo aead chunkSize salt s2k passphrase payload mSigs = do+ let packets = case mSigs of+ Nothing ->+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ (BL.fromStrict payload)+ ]+ Just sigs ->+ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ (BL.fromStrict payload)+ : sigs+ blockPayload = Block packets+ encryptSEIPDv2WithSKESKBlock+ symalgo+ aead+ chunkSize+ salt+ s2k+ passphrase+ blockPayload
Codec/Encryption/OpenPGP/Internal.hs view
@@ -2,35 +2,33 @@ -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).- {-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} module Codec.Encryption.OpenPGP.Internal- ( countBits- , PktStreamContext(..)- , issuer- , issuerFP- , emptyPSC- , leftPadTo- , pubkeyToMPIs- , multiplicativeInverse- , curveoidBSToCurve- , curveToCurveoidBS- , point2MBS- , curveoidBSToEdSigningCurve- , edSigningCurveToCurveoidBS- , curve2Curve- , curveFromCurve- ) where+ ( countBits+ , PktStreamContext (..)+ , issuer+ , issuerFP+ , emptyPSC+ , leftPadTo+ , pubkeyToMPIs+ , multiplicativeInverse+ , curveoidBSToCurve+ , curveToCurveoidBS+ , point2MBS+ , curveoidBSToEdSigningCurve+ , edSigningCurveToCurveoidBS+ , curve2Curve+ , curveFromCurve+ ) where import Crypto.Number.Serialize (i2osp, os2ip) import qualified Crypto.PubKey.DSA as DSA import qualified Crypto.PubKey.ECC.ECDSA as ECDSA import qualified Crypto.PubKey.ECC.Types as ECCT import qualified Crypto.PubKey.RSA as RSA- import Data.Bits (testBit) import qualified Data.ByteString as B import Data.ByteString.Lazy (ByteString)@@ -38,24 +36,28 @@ import Data.List (find) import Data.Word (Word16, Word8) -import Codec.Encryption.OpenPGP.Ontology (isIssuerSSP, isSigCreationTime)+import Codec.Encryption.OpenPGP.Ontology+ ( isIssuerSSP+ , isSigCreationTime+ ) import Codec.Encryption.OpenPGP.Types countBits :: ByteString -> Word16 countBits bs- | BL.null bs = 0- | otherwise =- fromIntegral (BL.length bs * 8) - fromIntegral (go (BL.head bs) 7)+ | BL.null bs = 0+ | otherwise =+ fromIntegral (BL.length bs * 8)+ - fromIntegral (go (BL.head bs) 7) where go :: Word8 -> Int -> Word8 go _ 0 = 7 go n b =- if testBit n b- then 7 - fromIntegral b- else go n (b - 1)+ if testBit n b+ then 7 - fromIntegral b+ else go n (b - 1) -data PktStreamContext =- PktStreamContext+data PktStreamContext+ = PktStreamContext { lastLD :: Pkt , lastUIDorUAt :: Pkt , lastSig :: Pkt@@ -65,47 +67,50 @@ emptyPSC :: PktStreamContext emptyPSC =- PktStreamContext- (OtherPacketPkt 0 "lastLD placeholder")- (OtherPacketPkt 0 "lastUIDorUAt placeholder")- (OtherPacketPkt 0 "lastSig placeholder")- (OtherPacketPkt 0 "lastPrimaryKey placeholder")- (OtherPacketPkt 0 "lastSubkey placeholder")+ PktStreamContext+ (OtherPacketPkt 0 "lastLD placeholder")+ (OtherPacketPkt 0 "lastUIDorUAt placeholder")+ (OtherPacketPkt 0 "lastSig placeholder")+ (OtherPacketPkt 0 "lastPrimaryKey placeholder")+ (OtherPacketPkt 0 "lastSubkey placeholder") leftPadTo :: Int -> B.ByteString -> B.ByteString leftPadTo targetLen bs- | B.length bs >= targetLen = bs- | otherwise = B.replicate (targetLen - B.length bs) 0 <> bs+ | B.length bs >= targetLen = bs+ | otherwise = B.replicate (targetLen - B.length bs) 0 <> bs issuer :: Pkt -> Maybe EightOctetKeyId issuer pkt =- case fromPktIssuerExtractionCase pkt of- Just extractionCase ->- find isIssuerSSP (unhashedSubpackets extractionCase) >>= issuerFromSubpacket- Nothing -> Nothing+ case fromPktIssuerExtractionCase pkt of+ Just extractionCase ->+ find isIssuerSSP (unhashedSubpackets extractionCase)+ >>= issuerFromSubpacket+ Nothing -> Nothing issuerFP :: Pkt -> Maybe Fingerprint issuerFP pkt =- case fromPktIssuerExtractionCase pkt of- Just extractionCase ->- find- (isIssuerFingerprintFor (issuerFingerprintVersion extractionCase))- (hashedSubpackets extractionCase) >>=- issuerFingerprintFromSubpacket- Nothing -> Nothing+ case fromPktIssuerExtractionCase pkt of+ Just extractionCase ->+ find+ (isIssuerFingerprintFor (issuerFingerprintVersion extractionCase))+ (hashedSubpackets extractionCase)+ >>= issuerFingerprintFromSubpacket+ Nothing -> Nothing data IssuerExtractionCase where- IssuerExtractionCaseV4 :: SignaturePayloadV 'SigPayloadV4 -> IssuerExtractionCase- IssuerExtractionCaseV6 :: SignaturePayloadV 'SigPayloadV6 -> IssuerExtractionCase+ IssuerExtractionCaseV4+ :: SignaturePayloadV 'SigPayloadV4 -> IssuerExtractionCase+ IssuerExtractionCaseV6+ :: SignaturePayloadV 'SigPayloadV6 -> IssuerExtractionCase fromPktIssuerExtractionCase :: Pkt -> Maybe IssuerExtractionCase fromPktIssuerExtractionCase pkt =- case fromPktEitherSomeSignatureV pkt of- Right (SomeSignatureV (SignatureV4Packet payload)) ->- Just (IssuerExtractionCaseV4 payload)- Right (SomeSignatureV (SignatureV6Packet payload)) ->- Just (IssuerExtractionCaseV6 payload)- _ -> Nothing+ case fromPktEitherSomeSignatureV pkt of+ Right (SomeSignatureV (SignatureV4Packet payload)) ->+ Just (IssuerExtractionCaseV4 payload)+ Right (SomeSignatureV (SignatureV6Packet payload)) ->+ Just (IssuerExtractionCaseV6 payload)+ _ -> Nothing hashedSubpackets :: IssuerExtractionCase -> [SigSubPacket] hashedSubpackets (IssuerExtractionCaseV4 (SigPayloadV4Data _ _ _ hsubs _ _ _)) = hsubs@@ -115,16 +120,19 @@ unhashedSubpackets (IssuerExtractionCaseV4 (SigPayloadV4Data _ _ _ _ usubs _ _)) = usubs unhashedSubpackets (IssuerExtractionCaseV6 (SigPayloadV6Data _ _ _ _ _ usubs _ _)) = usubs -issuerFingerprintVersion :: IssuerExtractionCase -> IssuerFingerprintVersion+issuerFingerprintVersion+ :: IssuerExtractionCase -> IssuerFingerprintVersion issuerFingerprintVersion IssuerExtractionCaseV4 {} = IssuerFingerprintV4 issuerFingerprintVersion IssuerExtractionCaseV6 {} = IssuerFingerprintV6 -isIssuerFingerprintFor :: IssuerFingerprintVersion -> SigSubPacket -> Bool+isIssuerFingerprintFor+ :: IssuerFingerprintVersion -> SigSubPacket -> Bool isIssuerFingerprintFor version (SigSubPacket _ (IssuerFingerprint packetVersion _)) =- packetVersion == version+ packetVersion == version isIssuerFingerprintFor _ _ = False -issuerFingerprintFromSubpacket :: SigSubPacket -> Maybe Fingerprint+issuerFingerprintFromSubpacket+ :: SigSubPacket -> Maybe Fingerprint issuerFingerprintFromSubpacket (SigSubPacket _ (IssuerFingerprint _ i)) = Just i issuerFingerprintFromSubpacket _ = Nothing @@ -134,28 +142,33 @@ pubkeyToMPIs :: PKey -> [MPI] pubkeyToMPIs (RSAPubKey (RSA_PublicKey k)) =- [MPI (RSA.public_n k), MPI (RSA.public_e k)]+ [MPI (RSA.public_n k), MPI (RSA.public_e k)] pubkeyToMPIs (DSAPubKey (DSA_PublicKey k)) =- [ pkParams DSA.params_p- , pkParams DSA.params_q- , pkParams DSA.params_g- , MPI . DSA.public_y $ k- ]+ [ pkParams DSA.params_p+ , pkParams DSA.params_q+ , pkParams DSA.params_g+ , MPI . DSA.public_y $ k+ ] where pkParams f = MPI . f . DSA.public_params $ k pubkeyToMPIs (ElGamalPubKey p g y) = [MPI p, MPI g, MPI y]-pubkeyToMPIs (ECDHPubKey (ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey _ q))) _ _) =- [MPI (os2ip (pointToBSOrError q))]+pubkeyToMPIs+ ( ECDHPubKey+ (ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey _ q)))+ _+ _+ ) =+ [MPI (os2ip (pointToBSOrError q))] pubkeyToMPIs (ECDHPubKey (EdDSAPubKey _ ep) _ _) = [MPI (edPointInteger ep)] pubkeyToMPIs (ECDSAPubKey ((ECDSA_PublicKey (ECDSA.PublicKey _ q)))) =- [MPI (os2ip (pointToBSOrError q))]+ [MPI (os2ip (pointToBSOrError q))] pubkeyToMPIs (EdDSAPubKey _ ep) = [MPI (edPointInteger ep)] edPointInteger :: EdPoint -> Integer edPointInteger (PrefixedNativeEPoint (EPoint x)) = x edPointInteger (NativeEPoint (EPoint x)) = x -multiplicativeInverse :: Integral a => a -> a -> a+multiplicativeInverse :: (Integral a) => a -> a -> a multiplicativeInverse _ 1 = 1 multiplicativeInverse q p = (n * q + 1) `div` p where@@ -163,31 +176,37 @@ curveoidBSToCurve :: B.ByteString -> Either String ECCCurve curveoidBSToCurve oidbs- | B.pack [0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07] == oidbs =- Right $ NISTP256 -- ECCT.getCurveByName ECCT.SEC_p256r1- | B.pack [0x2B, 0x81, 0x04, 0x00, 0x22] == oidbs = Right $ NISTP384 -- ECCT.getCurveByName ECCT.SEC_p384r1- | B.pack [0x2B, 0x81, 0x04, 0x00, 0x23] == oidbs = Right $ NISTP521 -- ECCT.getCurveByName ECCT.SEC_p521r1- | B.pack [0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01] == oidbs =- Right Curve25519- | B.pack [0x2B, 0x65, 0x6F] == oidbs =- Right Curve448- | otherwise = Left $ concat ["unknown curve (...", show (B.unpack oidbs), ")"]+ | B.pack [0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07] == oidbs =+ Right $ NISTP256 -- ECCT.getCurveByName ECCT.SEC_p256r1+ | B.pack [0x2B, 0x81, 0x04, 0x00, 0x22] == oidbs =+ Right $ NISTP384 -- ECCT.getCurveByName ECCT.SEC_p384r1+ | B.pack [0x2B, 0x81, 0x04, 0x00, 0x23] == oidbs =+ Right $ NISTP521 -- ECCT.getCurveByName ECCT.SEC_p521r1+ | B.pack+ [0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01]+ == oidbs =+ Right Curve25519+ | B.pack [0x2B, 0x65, 0x6F] == oidbs =+ Right Curve448+ | otherwise =+ Left $ concat ["unknown curve (...", show (B.unpack oidbs), ")"] curveToCurveoidBS :: ECCCurve -> Either String B.ByteString curveToCurveoidBS NISTP256 =- Right $ B.pack [0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]+ Right $ B.pack [0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07] curveToCurveoidBS NISTP384 = Right $ B.pack [0x2B, 0x81, 0x04, 0x00, 0x22] curveToCurveoidBS NISTP521 = Right $ B.pack [0x2B, 0x81, 0x04, 0x00, 0x23] curveToCurveoidBS Curve25519 =- Right $ B.pack [0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01]+ Right $+ B.pack+ [0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01] curveToCurveoidBS Curve448 = Right $ B.pack [0x2B, 0x65, 0x6F]-curveToCurveoidBS _ = Left "unknown curve" point2MBS :: ECCT.PublicPoint -> Maybe B.ByteString point2MBS (ECCT.Point x y)- | B.null xb || B.null yb = Nothing- | B.length xb /= B.length yb = Nothing- | otherwise = Just (B.concat [B.singleton 0x04, xb, yb])+ | B.null xb || B.null yb = Nothing+ | B.length xb /= B.length yb = Nothing+ | otherwise = Just (B.concat [B.singleton 0x04, xb, yb]) where xb = i2osp x yb = i2osp y@@ -195,29 +214,37 @@ pointToBSOrError :: ECCT.PublicPoint -> B.ByteString pointToBSOrError point =- case point of- ECCT.PointO -> error "OpenPGP forbids serializing the point at infinity"- _ ->- case point2MBS point of- Just bs -> bs- Nothing ->- error- "OpenPGP EC point serialization requires equal non-empty coordinate widths"+ case point of+ ECCT.PointO -> error "OpenPGP forbids serializing the point at infinity"+ _ ->+ case point2MBS point of+ Just bs -> bs+ Nothing ->+ error+ "OpenPGP EC point serialization requires equal non-empty coordinate widths" -curveoidBSToEdSigningCurve :: B.ByteString -> Either String EdSigningCurve+curveoidBSToEdSigningCurve+ :: B.ByteString -> Either String EdSigningCurve curveoidBSToEdSigningCurve oidbs- | B.pack [0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01] == oidbs =- Right Ed25519- | B.pack [0x2B, 0x65, 0x71] == oidbs =- Right Ed448- | otherwise =- Left $- concat ["unknown Edwards signing curve (...", show (B.unpack oidbs), ")"]+ | B.pack [0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01]+ == oidbs =+ Right EdSigningCurve25519+ | B.pack [0x2B, 0x65, 0x71] == oidbs =+ Right EdSigningCurve448+ | otherwise =+ Left $+ concat+ [ "unknown Edwards signing curve (..."+ , show (B.unpack oidbs)+ , ")"+ ] -edSigningCurveToCurveoidBS :: EdSigningCurve -> Either String B.ByteString-edSigningCurveToCurveoidBS Ed25519 =- Right $ B.pack [0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01]-edSigningCurveToCurveoidBS Ed448 = Right $ B.pack [0x2B, 0x65, 0x71]+edSigningCurveToCurveoidBS+ :: EdSigningCurve -> Either String B.ByteString+edSigningCurveToCurveoidBS EdSigningCurve25519 =+ Right $+ B.pack [0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01]+edSigningCurveToCurveoidBS EdSigningCurve448 = Right $ B.pack [0x2B, 0x65, 0x71] curve2Curve :: ECCCurve -> ECCT.Curve curve2Curve NISTP256 = ECCT.getCurveByName ECCT.SEC_p256r1@@ -226,6 +253,6 @@ curveFromCurve :: ECCT.Curve -> ECCCurve curveFromCurve c- | c == ECCT.getCurveByName ECCT.SEC_p256r1 = NISTP256- | c == ECCT.getCurveByName ECCT.SEC_p384r1 = NISTP384- | c == ECCT.getCurveByName ECCT.SEC_p521r1 = NISTP521+ | c == ECCT.getCurveByName ECCT.SEC_p256r1 = NISTP256+ | c == ECCT.getCurveByName ECCT.SEC_p384r1 = NISTP384+ | c == ECCT.getCurveByName ECCT.SEC_p521r1 = NISTP521
Codec/Encryption/OpenPGP/Internal/CryptoECDH.hs view
@@ -2,70 +2,84 @@ -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).- {-# LANGUAGE OverloadedStrings #-} module Codec.Encryption.OpenPGP.Internal.CryptoECDH- ( normalizeMontgomeryPublic- , buildECDHKDFParam- , deriveECDHKek- ) where+ ( normalizeMontgomeryPublic+ , buildECDHKDFParam+ , deriveECDHKek+ ) where -import Codec.Encryption.OpenPGP.BlockCipher (keySize, renderCipherError)-import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)-import Codec.Encryption.OpenPGP.Internal (curveFromCurve, curveToCurveoidBS, leftPadTo)-import Codec.Encryption.OpenPGP.Policy (ecdhKdfHashDigest)-import Codec.Encryption.OpenPGP.Types import qualified Crypto.PubKey.ECC.ECDSA as ECDSA import Data.Bifunctor (first) import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL -normalizeMontgomeryPublic ::- Int- -> String- -> B.ByteString- -> Either String B.ByteString+import Codec.Encryption.OpenPGP.BlockCipher+ ( keySize+ , renderCipherError+ )+import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)+import Codec.Encryption.OpenPGP.Internal+ ( curveFromCurve+ , curveToCurveoidBS+ , leftPadTo+ )+import Codec.Encryption.OpenPGP.Policy (ecdhKdfHashDigest)+import Codec.Encryption.OpenPGP.Types++normalizeMontgomeryPublic+ :: Int+ -> String+ -> B.ByteString+ -> Either String B.ByteString normalizeMontgomeryPublic targetLen label bs- | B.length bs == targetLen = Right bs- | B.length bs < targetLen = Right (leftPadTo targetLen bs)- | B.length bs == targetLen + 1 && B.head bs == 0x40 = Right (B.tail bs)- | otherwise = Left (label ++ show (B.length bs))+ | B.length bs == targetLen = Right bs+ | B.length bs < targetLen = Right (leftPadTo targetLen bs)+ | B.length bs == targetLen + 1 && B.head bs == 0x40 =+ Right (B.tail bs)+ | otherwise = Left (label ++ show (B.length bs)) -buildECDHKDFParam ::- SomePKPayload- -> PubKeyAlgorithm- -> PKey- -> HashAlgorithm- -> SymmetricAlgorithm- -> Either String B.ByteString+buildECDHKDFParam+ :: SomePKPayload+ -> PubKeyAlgorithm+ -> PKey+ -> HashAlgorithm+ -> SymmetricAlgorithm+ -> Either String B.ByteString buildECDHKDFParam recipientPKP pka recipientECDHPub kdfHA kdfSA =- (<>- B.pack [fromFVal pka, 0x03, 0x01, fromFVal kdfHA, fromFVal kdfSA] <>- "Anonymous Sender " <>- BL.toStrict (unFingerprint (fingerprint recipientPKP))) <$>- encodedCurveOid+ ( <>+ B.pack [fromFVal pka, 0x03, 0x01, fromFVal kdfHA, fromFVal kdfSA]+ <> "Anonymous Sender "+ <> BL.toStrict (unFingerprint (fingerprint recipientPKP))+ )+ <$> encodedCurveOid where- encodedCurveOid = ((\oid -> B.singleton (fromIntegral (B.length oid)) <> oid) <$>) curveOid+ encodedCurveOid =+ ((\oid -> B.singleton (fromIntegral (B.length oid)) <> oid) <$>)+ curveOid curveOid =- case recipientECDHPub of- ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _)) ->- curveToCurveoidBS (curveFromCurve curve)- EdDSAPubKey Ed25519 _ ->- curveToCurveoidBS Curve25519- EdDSAPubKey Ed448 _ ->- curveToCurveoidBS Curve448- _ -> Left "ECDH KDF param requires ECDH recipient key"+ case recipientECDHPub of+ ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _)) ->+ curveToCurveoidBS (curveFromCurve curve)+ EdDSAPubKey EdSigningCurve25519 _ ->+ curveToCurveoidBS Curve25519+ EdDSAPubKey EdSigningCurve448 _ ->+ curveToCurveoidBS Curve448+ _ -> Left "ECDH KDF param requires ECDH recipient key" -deriveECDHKek ::- HashAlgorithm- -> SymmetricAlgorithm- -> B.ByteString- -> B.ByteString- -> Either String B.ByteString+deriveECDHKek+ :: HashAlgorithm+ -> SymmetricAlgorithm+ -> B.ByteString+ -> B.ByteString+ -> Either String B.ByteString deriveECDHKek kdfHA kdfSA sharedSecret kdfParam = do- digest <- ecdhKdfHashDigest kdfHA (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam)- kekLen <- first renderCipherError (keySize kdfSA)- if B.length digest < kekLen- then Left "ECDH KDF digest is shorter than required KEK length"- else Right (B.take kekLen digest)+ digest <-+ ecdhKdfHashDigest+ kdfHA+ (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam)+ kekLen <- first renderCipherError (keySize kdfSA)+ if B.length digest < kekLen+ then Left "ECDH KDF digest is shorter than required KEK length"+ else Right (B.take kekLen digest)
Codec/Encryption/OpenPGP/KeyInfo.hs view
@@ -4,9 +4,9 @@ -- (See the LICENSE file). module Codec.Encryption.OpenPGP.KeyInfo- ( pubkeySize- , pkalgoAbbrev- ) where+ ( pubkeySize+ , pkalgoAbbrev+ ) where import qualified Crypto.PubKey.DSA as DSA import qualified Crypto.PubKey.ECC.ECDSA as ECDSA@@ -21,27 +21,33 @@ pubkeySize :: PKey -> Either String Int pubkeySize (RSAPubKey (RSA_PublicKey x)) = Right (RSA.public_size x * 8) pubkeySize (DSAPubKey (DSA_PublicKey x)) =- Right (bitcount . DSA.params_p . DSA.public_params $ x)+ Right (bitcount . DSA.params_p . DSA.public_params $ x) pubkeySize (ElGamalPubKey p _ _) = Right (bitcount p) pubkeySize (ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _))) =- Right (fromIntegral (ECCT.curveSizeBits curve))-pubkeySize (ECDHPubKey (ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _))) _ _) =- Right (fromIntegral (ECCT.curveSizeBits curve))-pubkeySize (ECDHPubKey (EdDSAPubKey Ed25519 _) _ _) = Right 256-pubkeySize (ECDHPubKey (EdDSAPubKey Ed448 _) _ _) = Right 448-pubkeySize (EdDSAPubKey Ed25519 _) = Right 256-pubkeySize (EdDSAPubKey Ed448 _) = Right 448+ Right (fromIntegral (ECCT.curveSizeBits curve))+pubkeySize+ ( ECDHPubKey+ (ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _)))+ _+ _+ ) =+ Right (fromIntegral (ECCT.curveSizeBits curve))+pubkeySize (ECDHPubKey (EdDSAPubKey EdSigningCurve25519 _) _ _) = Right 256+pubkeySize (ECDHPubKey (EdDSAPubKey EdSigningCurve448 _) _ _) = Right 448+pubkeySize (EdDSAPubKey EdSigningCurve25519 _) = Right 256+pubkeySize (EdDSAPubKey EdSigningCurve448 _) = Right 448 pubkeySize x = Left $ "Unable to calculate size of " ++ show x bitcount :: Integer -> Int bitcount =- (* 8) .- length .- unfoldr- (\x ->- if x == 0- then Nothing- else Just (True, x `shiftR` 8))+ (* 8)+ . length+ . unfoldr+ ( \x ->+ if x == 0+ then Nothing+ else Just (True, x `shiftR` 8)+ ) pkalgoAbbrev :: PubKeyAlgorithm -> String pkalgoAbbrev RSA = "rsa"
Codec/Encryption/OpenPGP/KeyringParser.hs view
@@ -2,169 +2,182 @@ -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).- {-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE LambdaCase #-} module Codec.Encryption.OpenPGP.KeyringParser- (- -- * Parsers- parseAChunk- , parseAChunkEither- , finalizeParsing- , finalizeParsingEither- , KeyringChunkParseError(..)- , anyTK- , anyTKWithWireRep- , UidOrUat(..)- , splitUs- , publicTK- , publicTKWithWireRep- , secretTK- , secretTKWithWireRep- , brokenTK- , brokenTKWithWireRep- , pkPayload- , pkPayloadWithWireRep- , signature- , signatureWithWireRep- , signedUID- , signedUIDWithWireRep- , signedUAt- , signedUAtWithWireRep- , signedOrRevokedPubSubkey- , signedOrRevokedPubSubkeyWithWireRep- , brokenPubSubkey- , brokenPubSubkeyWithWireRep- , rawOrSignedOrRevokedSecSubkey- , rawOrSignedOrRevokedSecSubkeyWithWireRep- , brokenSecSubkey- , brokenSecSubkeyWithWireRep- , skPayload- , skPayloadWithWireRep- , broken- , brokenWithWireRep- -- * Utilities- , parseUnknownTKs- , parseTKsEither- , parseTKs- , parsePublicTKs- , parseSecretTKs- , parseTKsWithWireRep- ) where+ ( -- * Parsers+ parseAChunk+ , parseAChunkEither+ , finalizeParsing+ , finalizeParsingEither+ , KeyringChunkParseError (..)+ , anyTK+ , anyTKWithWireRep+ , UidOrUat (..)+ , splitUs+ , publicTK+ , publicTKWithWireRep+ , secretTK+ , secretTKWithWireRep+ , brokenTK+ , brokenTKWithWireRep+ , pkPayload+ , pkPayloadWithWireRep+ , signature+ , signatureWithWireRep+ , signedUID+ , signedUIDWithWireRep+ , signedUAt+ , signedUAtWithWireRep+ , signedOrRevokedPubSubkey+ , signedOrRevokedPubSubkeyWithWireRep+ , brokenPubSubkey+ , brokenPubSubkeyWithWireRep+ , rawOrSignedOrRevokedSecSubkey+ , rawOrSignedOrRevokedSecSubkeyWithWireRep+ , brokenSecSubkey+ , brokenSecSubkeyWithWireRep+ , skPayload+ , skPayloadWithWireRep+ , broken+ , brokenWithWireRep -import Control.Applicative ((<|>), many)+ -- * Utilities+ , parseUnknownTKs+ , parseTKsEither+ , parseTKs+ , parsePublicTKs+ , parseSecretTKs+ , parseTKsWithWireRep+ ) where++import Control.Applicative (many, (<|>)) import Data.Either (rights)-import Data.List (foldl')-import Data.Maybe (catMaybes, mapMaybe) import qualified Data.List.NonEmpty as NE-+import Data.Maybe (catMaybes, mapMaybe) import Data.Text (Text)+import Text.ParserCombinators.Incremental.LeftBiasedLocal+ ( Parser+ , concatMany+ , failure+ , feed+ , feedEof+ , inspect+ , satisfy+ ) import Codec.Encryption.OpenPGP.Ontology (isTrustPkt) import Codec.Encryption.OpenPGP.Policy- ( isAllowedPrimaryKeySigType- , isAllowedSubkeySigType- , isAllowedUIDSigType- )+ ( isAllowedPrimaryKeySigType+ , isAllowedSubkeySigType+ , isAllowedUIDSigType+ ) import Codec.Encryption.OpenPGP.SignatureQualities (sigType) import Codec.Encryption.OpenPGP.Types import Data.Conduit.OpenPGP.Keyring.Instances ()-import Text.ParserCombinators.Incremental.LeftBiasedLocal- ( Parser- , concatMany- , failure- , feed- , feedEof- , inspect- , satisfy- ) data KeyringChunkParseError- = ChunkFailureBeforeInput String- | ChunkUnexpectedFinalizationFailure- | ChunkParserFailure String- deriving (Eq, Show)+ = ChunkFailureBeforeInput String+ | ChunkUnexpectedFinalizationFailure+ | ChunkParserFailure String+ deriving (Eq, Show) renderChunkParseError :: KeyringChunkParseError -> String renderChunkParseError (ChunkFailureBeforeInput msg) = msg renderChunkParseError ChunkUnexpectedFinalizationFailure =- "Unexpected finalization failure"+ "Unexpected finalization failure" renderChunkParseError (ChunkParserFailure msg) = msg -collapseCompleted ::- Monoid s- => [(r, s)]- -> ([r], s)+collapseCompleted+ :: Monoid s+ => [(r, s)]+ -> ([r], s) collapseCompleted rs =- let (resultsRev, remainder) =- foldl'- (\(accResults, accRemainder) (result, rest) ->- (result : accResults, accRemainder <> rest))- ([], mempty)- rs- in (reverse resultsRev, remainder)+ let (resultsRev, remainder) =+ foldl'+ ( \(accResults, accRemainder) (result, rest) ->+ (result : accResults, accRemainder <> rest)+ )+ ([], mempty)+ rs+ in (reverse resultsRev, remainder) -parseAChunk ::- (Monoid s, Show s)- => Parser s r- -> s- -> ([(r, s)], Maybe (Maybe (r -> r), Parser s r))- -> (([(r, s)], Maybe (Maybe (r -> r), Parser s r)), [r])+parseAChunk+ :: (Monoid s, Show s)+ => Parser s r+ -> s+ -> ([(r, s)], Maybe (Maybe (r -> r), Parser s r))+ -> (([(r, s)], Maybe (Maybe (r -> r), Parser s r)), [r]) parseAChunk op a st =- either (error . renderChunkParseError) id (parseAChunkEither op a st)+ either+ (error . renderChunkParseError)+ id+ (parseAChunkEither op a st) -parseAChunkEither ::- (Monoid s, Show s)- => Parser s r- -> s- -> ([(r, s)], Maybe (Maybe (r -> r), Parser s r))- -> Either- KeyringChunkParseError- (([(r, s)], Maybe (Maybe (r -> r), Parser s r)), [r])+parseAChunkEither+ :: (Monoid s, Show s)+ => Parser s r+ -> s+ -> ([(r, s)], Maybe (Maybe (r -> r), Parser s r))+ -> Either+ KeyringChunkParseError+ (([(r, s)], Maybe (Maybe (r -> r), Parser s r)), [r]) parseAChunkEither _ a ([], Nothing) =- Left (ChunkFailureBeforeInput ("Failure before " ++ show a))+ Left (ChunkFailureBeforeInput ("Failure before " ++ show a)) parseAChunkEither op a (cr, Nothing) =- let (completed, remainder) = collapseCompleted cr- in- (\x -> (x, completed)) <$>- either (Left . ChunkParserFailure) Right (inspect (feed (remainder <> a) op))+ let (completed, remainder) = collapseCompleted cr+ in (\x -> (x, completed))+ <$> either+ (Left . ChunkParserFailure)+ Right+ (inspect (feed (remainder <> a) op)) parseAChunkEither _ a (_, Just (_, p)) =- (\x -> (x, [])) <$> either (Left . ChunkParserFailure) Right (inspect (feed a p))+ (\x -> (x, []))+ <$> either (Left . ChunkParserFailure) Right (inspect (feed a p)) -finalizeParsing ::- Monoid s- => ([(r, s)], Maybe (Maybe (r -> r), Parser s r))- -> (([(r, s)], Maybe (Maybe (r -> r), Parser s r)), [r])+finalizeParsing+ :: Monoid s+ => ([(r, s)], Maybe (Maybe (r -> r), Parser s r))+ -> (([(r, s)], Maybe (Maybe (r -> r), Parser s r)), [r]) finalizeParsing st =- either (error . renderChunkParseError) id (finalizeParsingEither st)+ either+ (error . renderChunkParseError)+ id+ (finalizeParsingEither st) -finalizeParsingEither ::- Monoid s- => ([(r, s)], Maybe (Maybe (r -> r), Parser s r))- -> Either- KeyringChunkParseError- (([(r, s)], Maybe (Maybe (r -> r), Parser s r)), [r])+finalizeParsingEither+ :: Monoid s+ => ([(r, s)], Maybe (Maybe (r -> r), Parser s r))+ -> Either+ KeyringChunkParseError+ (([(r, s)], Maybe (Maybe (r -> r), Parser s r)), [r]) finalizeParsingEither ([], Nothing) = Left ChunkUnexpectedFinalizationFailure finalizeParsingEither (cr, Nothing) =- let (completed, _) = collapseCompleted cr- in Right (([], Nothing), completed)+ let (completed, _) = collapseCompleted cr+ in Right (([], Nothing), completed) finalizeParsingEither (_, Just (_, p)) =- either (Left . ChunkParserFailure) finalizeParsingEither (inspect (feedEof p))+ either+ (Left . ChunkParserFailure)+ 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 False =+ publicTK False <|> secretTK False <|> brokenTK 6 <|> brokenTK 5 data UidOrUat- = I Text- | A [UserAttrSubPacket]- deriving (Show)+ = I Text+ | A [UserAttrSubPacket]+ deriving (Show) -splitUs ::- [(UidOrUat, [SignaturePayload])]- -> ([(Text, [SignaturePayload])], [([UserAttrSubPacket], [SignaturePayload])])+splitUs+ :: [(UidOrUat, [SignaturePayload])]+ -> ( [(Text, [SignaturePayload])]+ , [([UserAttrSubPacket], [SignaturePayload])]+ ) splitUs us = (is, as) where is = map unI (filter isI us)@@ -180,85 +193,99 @@ publicTK, secretTK :: Bool -> Parser [Pkt] (Maybe TKUnknown) publicTK intolerant = do- pkp <- pkPayload- pkpsigs <-- concatMany- (signatureWithPredicate intolerant isAllowedPrimaryKeySigType)- (uids, uats) <-- fmap splitUs (many (signedUID intolerant <|> signedUAt intolerant))- subs <- concatMany (pubsub intolerant)- return $ Just (TKUnknown pkp pkpsigs uids uats subs)+ pkp <- pkPayload+ pkpsigs <-+ concatMany+ (signatureWithPredicate intolerant isAllowedPrimaryKeySigType)+ (uids, uats) <-+ fmap+ splitUs+ (many (signedUID intolerant <|> signedUAt intolerant))+ subs <- concatMany (pubsub intolerant)+ return $ Just (TKUnknown pkp pkpsigs uids uats subs) where pubsub True = signedOrRevokedPubSubkey True pubsub False = signedOrRevokedPubSubkey False <|> brokenPubSubkey- secretTK intolerant = do- skp <- skPayload- skpsigs <-- concatMany- (signatureWithPredicate intolerant isAllowedPrimaryKeySigType)- (uids, uats) <-- fmap splitUs (many (signedUID intolerant <|> signedUAt intolerant))- subs <- concatMany (secsub intolerant)- return $ Just (TKUnknown skp skpsigs uids uats subs)+ skp <- skPayload+ skpsigs <-+ concatMany+ (signatureWithPredicate intolerant isAllowedPrimaryKeySigType)+ (uids, uats) <-+ fmap+ splitUs+ (many (signedUID intolerant <|> signedUAt intolerant))+ subs <- concatMany (secsub intolerant)+ return $ Just (TKUnknown skp skpsigs uids uats subs) where secsub True = rawOrSignedOrRevokedSecSubkey True secsub False = rawOrSignedOrRevokedSecSubkey False <|> brokenSecSubkey brokenTK :: Int -> Parser [Pkt] (Maybe TKUnknown) brokenTK 6 = do- _ <- broken 6- _ <- many (signature False [KeyRevocationSig, SignatureDirectlyOnAKey])- _ <- many (signedUID False <|> signedUAt False)- _ <- concatMany (signedOrRevokedPubSubkey False <|> brokenPubSubkey)- return Nothing+ _ <- broken 6+ _ <-+ many+ (signature False [KeyRevocationSig, SignatureDirectlyOnAKey])+ _ <- many (signedUID False <|> signedUAt False)+ _ <-+ concatMany (signedOrRevokedPubSubkey False <|> brokenPubSubkey)+ return Nothing brokenTK 5 = do- _ <- broken 5- _ <- many (signature False [KeyRevocationSig, SignatureDirectlyOnAKey])- _ <- many (signedUID False <|> signedUAt False)- _ <- concatMany (rawOrSignedOrRevokedSecSubkey False <|> brokenSecSubkey)- return Nothing+ _ <- broken 5+ _ <-+ many+ (signature False [KeyRevocationSig, SignatureDirectlyOnAKey])+ _ <- many (signedUID False <|> signedUAt False)+ _ <-+ concatMany+ (rawOrSignedOrRevokedSecSubkey False <|> brokenSecSubkey)+ return Nothing brokenTK _ = fail "Unexpected broken packet type" pkPayload :: Parser [Pkt] (SomePKPayload, Maybe SKAddendum) pkPayload = do- pkpkts <- satisfy isPKP- case pkpkts of- [pkt] ->- case pktToPublicKeyPkt pkt of- Just keyPkt | keyPktRole keyPkt == KeyPktPrimary ->- return (keyPktTKKey keyPkt)+ pkpkts <- satisfy isPKP+ case pkpkts of+ [pkt] ->+ case pktToPublicKeyPkt pkt of+ Just keyPkt+ | keyPktRole keyPkt == KeyPktPrimary ->+ return (keyPktTKKey keyPkt)+ _ -> failure _ -> failure- _ -> failure where isPKP [pkt] =- case pktToPublicKeyPkt pkt of- Just keyPkt -> keyPktRole keyPkt == KeyPktPrimary- Nothing -> False+ case pktToPublicKeyPkt pkt of+ Just keyPkt -> keyPktRole keyPkt == KeyPktPrimary+ Nothing -> False isPKP _ = False signature :: Bool -> [SigType] -> Parser [Pkt] [SignaturePayload] signature intolerant rts = signatureWithPredicate intolerant (\st -> st `elem` rts) --- | RFC9580-aware signature parser that validates context using a predicate--- The predicate operates on SigType to determine if the signature is allowed--- in this context (e.g., isAllowedPrimaryKeySigType for primary keys).-signatureWithPredicate :: Bool -> (SigType -> Bool) -> Parser [Pkt] [SignaturePayload]+{- | RFC9580-aware signature parser that validates context using a predicate+The predicate operates on SigType to determine if the signature is allowed+in this context (e.g., isAllowedPrimaryKeySigType for primary keys).+-}+signatureWithPredicate+ :: Bool -> (SigType -> Bool) -> Parser [Pkt] [SignaturePayload] signatureWithPredicate intolerant predicate =- if intolerant- then signature'- else signature' <|> brokensig'+ if intolerant+ then signature'+ else signature' <|> brokensig' where signature' = do- spks <- satisfy (isSP intolerant)- case spks of- [SignaturePkt sp] ->- return $!- (if intolerant- then id- else filter isSP')- [sp]- _ -> failure+ spks <- satisfy (isSP intolerant)+ case spks of+ [SignaturePkt sp] ->+ return $!+ ( if intolerant+ then id+ else filter isSP'+ )+ [sp]+ _ -> failure brokensig' = const [] <$> broken 2 isSP True [SignaturePkt sp] = isSP' sp isSP False [SignaturePkt _] = True@@ -267,102 +294,106 @@ signedUID :: Bool -> Parser [Pkt] (UidOrUat, [SignaturePayload]) signedUID intolerant = do- upkts <- satisfy isUID- case upkts of- [UserIdPkt u] -> do- sigs <-- concatMany- (signatureWithPredicate intolerant isAllowedUIDSigType)- return (I u, sigs)- _ -> failure+ upkts <- satisfy isUID+ case upkts of+ [UserIdPkt u] -> do+ sigs <-+ concatMany+ (signatureWithPredicate intolerant isAllowedUIDSigType)+ return (I u, sigs)+ _ -> failure where isUID [UserIdPkt _] = True isUID _ = False signedUAt :: Bool -> Parser [Pkt] (UidOrUat, [SignaturePayload]) signedUAt intolerant = do- uapkts <- satisfy isUAt- case uapkts of- [UserAttributePkt us] -> do- sigs <-- concatMany- (signatureWithPredicate intolerant isAllowedUIDSigType)- return (A us, sigs)- _ -> failure+ uapkts <- satisfy isUAt+ case uapkts of+ [UserAttributePkt us] -> do+ sigs <-+ concatMany+ (signatureWithPredicate intolerant isAllowedUIDSigType)+ return (A us, sigs)+ _ -> failure where isUAt [UserAttributePkt _] = True isUAt _ = False -signedOrRevokedPubSubkey :: Bool -> Parser [Pkt] [(Pkt, [SignaturePayload])]+signedOrRevokedPubSubkey+ :: Bool -> Parser [Pkt] [(Pkt, [SignaturePayload])] signedOrRevokedPubSubkey intolerant = do- pskpkts <- satisfy isPSKP- case pskpkts of- [p] -> do- sigs <-- concatMany- (signatureWithPredicate intolerant isAllowedSubkeySigType)- return [(p, sigs)]- _ -> failure+ pskpkts <- satisfy isPSKP+ case pskpkts of+ [p] -> do+ sigs <-+ concatMany+ (signatureWithPredicate intolerant isAllowedSubkeySigType)+ return [(p, sigs)]+ _ -> failure where isPSKP [pkt] =- case pktToPublicKeyPkt pkt of- Just keyPkt -> keyPktRole keyPkt == KeyPktSubkey- Nothing -> False+ case pktToPublicKeyPkt pkt of+ Just keyPkt -> keyPktRole keyPkt == KeyPktSubkey+ Nothing -> False isPSKP _ = False brokenPubSubkey :: Parser [Pkt] [(Pkt, [SignaturePayload])] brokenPubSubkey = do- _ <- broken 14- _ <- concatMany (signatureWithPredicate False isAllowedSubkeySigType)- return []+ _ <- broken 14+ _ <-+ concatMany (signatureWithPredicate False isAllowedSubkeySigType)+ return [] -rawOrSignedOrRevokedSecSubkey ::- Bool -> Parser [Pkt] [(Pkt, [SignaturePayload])]+rawOrSignedOrRevokedSecSubkey+ :: Bool -> Parser [Pkt] [(Pkt, [SignaturePayload])] rawOrSignedOrRevokedSecSubkey intolerant = do- sskpkts <- satisfy isSSKP- case sskpkts of- [p] -> do- sigs <-- concatMany- (signatureWithPredicate intolerant isAllowedSubkeySigType)- return [(p, sigs)]- _ -> failure+ sskpkts <- satisfy isSSKP+ case sskpkts of+ [p] -> do+ sigs <-+ concatMany+ (signatureWithPredicate intolerant isAllowedSubkeySigType)+ return [(p, sigs)]+ _ -> failure where isSSKP [pkt] =- case pktToSecretKeyPkt pkt of- Just keyPkt -> keyPktRole keyPkt == KeyPktSubkey- Nothing -> False+ case pktToSecretKeyPkt pkt of+ Just keyPkt -> keyPktRole keyPkt == KeyPktSubkey+ Nothing -> False isSSKP _ = False brokenSecSubkey :: Parser [Pkt] [(Pkt, [SignaturePayload])] brokenSecSubkey = do- _ <- broken 7- _ <- concatMany (signatureWithPredicate False isAllowedSubkeySigType)- return []+ _ <- broken 7+ _ <-+ concatMany (signatureWithPredicate False isAllowedSubkeySigType)+ return [] skPayload :: Parser [Pkt] (SomePKPayload, Maybe SKAddendum) skPayload = do- spkts <- satisfy isSKP- case spkts of- [pkt] ->- case pktToSecretKeyPkt pkt of- Just keyPkt | keyPktRole keyPkt == KeyPktPrimary ->- return (keyPktTKKey keyPkt)+ spkts <- satisfy isSKP+ case spkts of+ [pkt] ->+ case pktToSecretKeyPkt pkt of+ Just keyPkt+ | keyPktRole keyPkt == KeyPktPrimary ->+ return (keyPktTKKey keyPkt)+ _ -> failure _ -> failure- _ -> failure where isSKP [pkt] =- case pktToSecretKeyPkt pkt of- Just keyPkt -> keyPktRole keyPkt == KeyPktPrimary- Nothing -> False+ case pktToSecretKeyPkt pkt of+ Just keyPkt -> keyPktRole keyPkt == KeyPktPrimary+ Nothing -> False isSKP _ = False broken :: Int -> Parser [Pkt] Pkt broken t = do- bpkts <- satisfy isBroken- case bpkts of- [bp] -> return bp- _ -> failure+ bpkts <- satisfy isBroken+ case bpkts of+ [bp] -> return bp+ _ -> failure where isBroken [BrokenPacketPkt _ a _] = t == fromIntegral a isBroken _ = False@@ -370,309 +401,370 @@ -- | parse TKs from packets parseUnknownTKs :: Bool -> [Pkt] -> [TKUnknown] parseUnknownTKs intolerant ps =- catMaybes $- runIncrementalParser- (anyTK intolerant)- (map (: []) (filter notTrustPacket ps))+ catMaybes $+ runIncrementalParser+ (anyTK intolerant)+ (map (: []) (filter notTrustPacket ps)) where notTrustPacket = not . isTrustPkt -parseTKsEither :: Bool -> [Pkt] -> [Either TKConversionError SomeTK]+parseTKsEither+ :: Bool -> [Pkt] -> [Either TKConversionError SomeTK] parseTKsEither intolerant =- map fromUnknownToTKEither . parseUnknownTKs intolerant+ map fromUnknownToTKEither . parseUnknownTKs intolerant parseTKs :: Bool -> [Pkt] -> [SomeTK] parseTKs intolerant packets = rights (parseTKsEither intolerant packets) parsePublicTKs :: Bool -> [Pkt] -> [TK 'PublicTK] parsePublicTKs intolerant packets =- mapMaybe someTKToPublicTK (parseTKs intolerant packets)+ mapMaybe someTKToPublicTK (parseTKs intolerant packets) parseSecretTKs :: Bool -> [Pkt] -> [TK 'SecretTK] parseSecretTKs intolerant packets =- mapMaybe someTKToSecretTK (parseTKs intolerant packets)+ mapMaybe someTKToSecretTK (parseTKs intolerant packets) -anyTKWithWireRep :: Bool -> Parser [PktWithWireRep] (Maybe TKWithWireRep)+anyTKWithWireRep+ :: Bool -> Parser [PktWithWireRep] (Maybe TKWithWireRep) anyTKWithWireRep True = publicTKWithWireRep True <|> secretTKWithWireRep True anyTKWithWireRep False =- publicTKWithWireRep False <|> secretTKWithWireRep False <|>- brokenTKWithWireRep 6 <|> brokenTKWithWireRep 5+ publicTKWithWireRep False+ <|> secretTKWithWireRep False+ <|> brokenTKWithWireRep 6+ <|> brokenTKWithWireRep 5 -publicTKWithWireRep, secretTKWithWireRep ::- Bool -> Parser [PktWithWireRep] (Maybe TKWithWireRep)+publicTKWithWireRep+ , secretTKWithWireRep+ :: Bool -> Parser [PktWithWireRep] (Maybe TKWithWireRep) publicTKWithWireRep intolerant = do- (pkp, pkps) <- pkPayloadWithWireRep- (pkpsigs, pkpsigrefs) <-- concatMany- (signatureWithWireRepPredicate intolerant isAllowedPrimaryKeySigType)- uidResults <- many (signedUIDWithWireRep intolerant <|> signedUAtWithWireRep intolerant)- subResults <- concatMany (pubsub intolerant)- let semanticUs = fmap fst uidResults- (uids, uats) = splitUs semanticUs- uidrefs = concatMap snd uidResults- subs = fmap fst subResults- subrefs = concatMap snd subResults- tk = TKUnknown pkp pkpsigs uids uats subs- refs = pkps ++ pkpsigrefs ++ uidrefs ++ subrefs- return $ Just (mkTKWithWireRep tk refs)+ (pkp, pkps) <- pkPayloadWithWireRep+ (pkpsigs, pkpsigrefs) <-+ concatMany+ ( signatureWithWireRepPredicate+ intolerant+ isAllowedPrimaryKeySigType+ )+ uidResults <-+ many+ ( signedUIDWithWireRep intolerant+ <|> signedUAtWithWireRep intolerant+ )+ subResults <- concatMany (pubsub intolerant)+ let semanticUs = fmap fst uidResults+ (uids, uats) = splitUs semanticUs+ uidrefs = concatMap snd uidResults+ subs = fmap fst subResults+ subrefs = concatMap snd subResults+ tk = TKUnknown pkp pkpsigs uids uats subs+ refs = pkps ++ pkpsigrefs ++ uidrefs ++ subrefs+ return $ Just (mkTKWithWireRep tk refs) where pubsub True = signedOrRevokedPubSubkeyWithWireRep True pubsub False =- signedOrRevokedPubSubkeyWithWireRep False <|> brokenPubSubkeyWithWireRep-+ 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- tk = TKUnknown skp skpsigs uids uats subs- refs = skps ++ skpsigrefs ++ uidrefs ++ subrefs- return $ Just (mkTKWithWireRep tk refs)+ (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+ tk = TKUnknown skp skpsigs uids uats subs+ refs = skps ++ skpsigrefs ++ uidrefs ++ subrefs+ return $ Just (mkTKWithWireRep tk refs) where secsub True = rawOrSignedOrRevokedSecSubkeyWithWireRep True secsub False =- rawOrSignedOrRevokedSecSubkeyWithWireRep False <|> brokenSecSubkeyWithWireRep+ rawOrSignedOrRevokedSecSubkeyWithWireRep False+ <|> brokenSecSubkeyWithWireRep -brokenTKWithWireRep :: Int -> Parser [PktWithWireRep] (Maybe TKWithWireRep)+brokenTKWithWireRep+ :: Int -> Parser [PktWithWireRep] (Maybe TKWithWireRep) brokenTKWithWireRep 6 = do- _ <- brokenWithWireRep 6- _ <- many (signatureWithWireRepPredicate False isAllowedPrimaryKeySigType)- _ <- many (signedUIDWithWireRep False <|> signedUAtWithWireRep False)- _ <-- concatMany- (signedOrRevokedPubSubkeyWithWireRep False <|> brokenPubSubkeyWithWireRep)- return Nothing+ _ <- brokenWithWireRep 6+ _ <-+ many+ (signatureWithWireRepPredicate False isAllowedPrimaryKeySigType)+ _ <-+ many (signedUIDWithWireRep False <|> signedUAtWithWireRep False)+ _ <-+ concatMany+ ( signedOrRevokedPubSubkeyWithWireRep False+ <|> brokenPubSubkeyWithWireRep+ )+ return Nothing brokenTKWithWireRep 5 = do- _ <- brokenWithWireRep 5- _ <- many (signatureWithWireRepPredicate False isAllowedPrimaryKeySigType)- _ <- many (signedUIDWithWireRep False <|> signedUAtWithWireRep False)- _ <-- concatMany- (rawOrSignedOrRevokedSecSubkeyWithWireRep False <|> brokenSecSubkeyWithWireRep)- return Nothing+ _ <- brokenWithWireRep 5+ _ <-+ many+ (signatureWithWireRepPredicate False isAllowedPrimaryKeySigType)+ _ <-+ many (signedUIDWithWireRep False <|> signedUAtWithWireRep False)+ _ <-+ concatMany+ ( rawOrSignedOrRevokedSecSubkeyWithWireRep False+ <|> brokenSecSubkeyWithWireRep+ )+ return Nothing brokenTKWithWireRep _ = fail "Unexpected broken packet type" -pkPayloadWithWireRep ::- Parser [PktWithWireRep] ((SomePKPayload, Maybe SKAddendum), [PktWithWireRep])+pkPayloadWithWireRep+ :: Parser+ [PktWithWireRep]+ ((SomePKPayload, Maybe SKAddendum), [PktWithWireRep]) pkPayloadWithWireRep = do- pkpkts <- satisfy isPKPWS- case pkpkts of- [pktWithSource] ->- case pktToPublicKeyPkt (_pktValue pktWithSource) of- Just keyPkt | keyPktRole keyPkt == KeyPktPrimary ->- return (keyPktTKKey keyPkt, [pktWithSource])+ pkpkts <- satisfy isPKPWS+ case pkpkts of+ [pktWithSource] ->+ case pktToPublicKeyPkt (_pktValue pktWithSource) of+ Just keyPkt+ | keyPktRole keyPkt == KeyPktPrimary ->+ return (keyPktTKKey keyPkt, [pktWithSource])+ _ -> failure _ -> failure- _ -> failure where isPKPWS [pktWithSource] =- case pktToPublicKeyPkt (_pktValue pktWithSource) of- Just keyPkt -> keyPktRole keyPkt == KeyPktPrimary- _ -> False+ case pktToPublicKeyPkt (_pktValue pktWithSource) of+ Just keyPkt -> keyPktRole keyPkt == KeyPktPrimary+ _ -> False isPKPWS _ = False -signatureWithWireRep ::- Bool- -> [SigType]- -> Parser [PktWithWireRep] ([SignaturePayload], [PktWithWireRep])+signatureWithWireRep+ :: Bool+ -> [SigType]+ -> Parser [PktWithWireRep] ([SignaturePayload], [PktWithWireRep]) signatureWithWireRep intolerant rts =- signatureWithWireRepPredicate intolerant (\st -> st `elem` rts)+ signatureWithWireRepPredicate intolerant (\st -> st `elem` rts) -- | RFC9580-aware signature parser with wire representation support-signatureWithWireRepPredicate ::- Bool- -> (SigType -> Bool)- -> Parser [PktWithWireRep] ([SignaturePayload], [PktWithWireRep])+signatureWithWireRepPredicate+ :: Bool+ -> (SigType -> Bool)+ -> Parser [PktWithWireRep] ([SignaturePayload], [PktWithWireRep]) signatureWithWireRepPredicate intolerant predicate =- if intolerant- then signature'- else signature' <|> brokensig'+ if intolerant+ then signature'+ else signature' <|> brokensig' where signature' = do- spks <- satisfy (isSPWS intolerant)- case spks of- [pktWithSource] ->- case _pktValue pktWithSource of- SignaturePkt sp ->- let sigs =- (if intolerant- then id- else filter isSP')- [sp]- in return (sigs, if null sigs then [] else [pktWithSource])+ spks <- satisfy (isSPWS intolerant)+ case spks of+ [pktWithSource] ->+ case _pktValue pktWithSource of+ SignaturePkt sp ->+ let sigs =+ ( if intolerant+ then id+ else filter isSP'+ )+ [sp]+ in return (sigs, if null sigs then [] else [pktWithSource])+ _ -> failure _ -> failure- _ -> failure brokensig' = const ([], []) <$> brokenWithWireRep 2 isSPWS True [pktWithSource] =- case _pktValue pktWithSource of- SignaturePkt sp -> isSP' sp- _ -> False+ case _pktValue pktWithSource of+ SignaturePkt sp -> isSP' sp+ _ -> False isSPWS False [pktWithSource] =- case _pktValue pktWithSource of- SignaturePkt _ -> True- _ -> False+ case _pktValue pktWithSource of+ SignaturePkt _ -> True+ _ -> False isSPWS _ _ = False isSP' sigPayload = maybe False predicate (sigType sigPayload) -signedUIDWithWireRep ::- Bool- -> Parser [PktWithWireRep] ((UidOrUat, [SignaturePayload]), [PktWithWireRep])+signedUIDWithWireRep+ :: Bool+ -> Parser+ [PktWithWireRep]+ ((UidOrUat, [SignaturePayload]), [PktWithWireRep]) signedUIDWithWireRep intolerant = do- upkts <- satisfy isUIDWS- case upkts of- [pktWithSource] ->- case _pktValue pktWithSource of- UserIdPkt u -> do- (sigs, sigrefs) <-- concatMany- (signatureWithWireRepPredicate intolerant isAllowedUIDSigType)- return ((I u, sigs), pktWithSource : sigrefs)+ upkts <- satisfy isUIDWS+ case upkts of+ [pktWithSource] ->+ case _pktValue pktWithSource of+ UserIdPkt u -> do+ (sigs, sigrefs) <-+ concatMany+ (signatureWithWireRepPredicate intolerant isAllowedUIDSigType)+ return ((I u, sigs), pktWithSource : sigrefs)+ _ -> failure _ -> failure- _ -> failure where isUIDWS [pktWithSource] =- case _pktValue pktWithSource of- UserIdPkt _ -> True- _ -> False+ case _pktValue pktWithSource of+ UserIdPkt _ -> True+ _ -> False isUIDWS _ = False -signedUAtWithWireRep ::- Bool- -> Parser [PktWithWireRep] ((UidOrUat, [SignaturePayload]), [PktWithWireRep])+signedUAtWithWireRep+ :: Bool+ -> Parser+ [PktWithWireRep]+ ((UidOrUat, [SignaturePayload]), [PktWithWireRep]) signedUAtWithWireRep intolerant = do- uapkts <- satisfy isUAtWS- case uapkts of- [pktWithSource] ->- case _pktValue pktWithSource of- UserAttributePkt us -> do- (sigs, sigrefs) <-- concatMany- (signatureWithWireRepPredicate intolerant isAllowedUIDSigType)- return ((A us, sigs), pktWithSource : sigrefs)+ uapkts <- satisfy isUAtWS+ case uapkts of+ [pktWithSource] ->+ case _pktValue pktWithSource of+ UserAttributePkt us -> do+ (sigs, sigrefs) <-+ concatMany+ (signatureWithWireRepPredicate intolerant isAllowedUIDSigType)+ return ((A us, sigs), pktWithSource : sigrefs)+ _ -> failure _ -> failure- _ -> failure where isUAtWS [pktWithSource] =- case _pktValue pktWithSource of- UserAttributePkt _ -> True- _ -> False+ case _pktValue pktWithSource of+ UserAttributePkt _ -> True+ _ -> False isUAtWS _ = False -signedOrRevokedPubSubkeyWithWireRep ::- Bool -> Parser [PktWithWireRep] [((Pkt, [SignaturePayload]), [PktWithWireRep])]+signedOrRevokedPubSubkeyWithWireRep+ :: Bool+ -> Parser+ [PktWithWireRep]+ [((Pkt, [SignaturePayload]), [PktWithWireRep])] signedOrRevokedPubSubkeyWithWireRep intolerant = do- pskpkts <- satisfy isPSKPWS- case pskpkts of- [pktWithSource] -> do- (sigs, sigrefs) <-- concatMany- (signatureWithWireRepPredicate intolerant isAllowedSubkeySigType)- return [((_pktValue pktWithSource, sigs), pktWithSource : sigrefs)]- _ -> failure+ pskpkts <- satisfy isPSKPWS+ case pskpkts of+ [pktWithSource] -> do+ (sigs, sigrefs) <-+ concatMany+ (signatureWithWireRepPredicate intolerant isAllowedSubkeySigType)+ return+ [((_pktValue pktWithSource, sigs), pktWithSource : sigrefs)]+ _ -> failure where isPSKPWS [pktWithSource] =- case pktToPublicKeyPkt (_pktValue pktWithSource) of- Just keyPkt -> keyPktRole keyPkt == KeyPktSubkey- _ -> False+ case pktToPublicKeyPkt (_pktValue pktWithSource) of+ Just keyPkt -> keyPktRole keyPkt == KeyPktSubkey+ _ -> False isPSKPWS _ = False -brokenPubSubkeyWithWireRep ::- Parser [PktWithWireRep] [((Pkt, [SignaturePayload]), [PktWithWireRep])]+brokenPubSubkeyWithWireRep+ :: Parser+ [PktWithWireRep]+ [((Pkt, [SignaturePayload]), [PktWithWireRep])] brokenPubSubkeyWithWireRep = do- _ <- brokenWithWireRep 14- _ <- concatMany (signatureWithWireRepPredicate False isAllowedSubkeySigType)- return []+ _ <- brokenWithWireRep 14+ _ <-+ concatMany+ (signatureWithWireRepPredicate False isAllowedSubkeySigType)+ return [] -rawOrSignedOrRevokedSecSubkeyWithWireRep ::- Bool -> Parser [PktWithWireRep] [((Pkt, [SignaturePayload]), [PktWithWireRep])]+rawOrSignedOrRevokedSecSubkeyWithWireRep+ :: Bool+ -> Parser+ [PktWithWireRep]+ [((Pkt, [SignaturePayload]), [PktWithWireRep])] rawOrSignedOrRevokedSecSubkeyWithWireRep intolerant = do- sskpkts <- satisfy isSSKPWS- case sskpkts of- [pktWithSource] -> do- (sigs, sigrefs) <-- concatMany- (signatureWithWireRepPredicate intolerant isAllowedSubkeySigType)- return [((_pktValue pktWithSource, sigs), pktWithSource : sigrefs)]- _ -> failure+ sskpkts <- satisfy isSSKPWS+ case sskpkts of+ [pktWithSource] -> do+ (sigs, sigrefs) <-+ concatMany+ (signatureWithWireRepPredicate intolerant isAllowedSubkeySigType)+ return+ [((_pktValue pktWithSource, sigs), pktWithSource : sigrefs)]+ _ -> failure where isSSKPWS [pktWithSource] =- case pktToSecretKeyPkt (_pktValue pktWithSource) of- Just keyPkt -> keyPktRole keyPkt == KeyPktSubkey- _ -> False+ case pktToSecretKeyPkt (_pktValue pktWithSource) of+ Just keyPkt -> keyPktRole keyPkt == KeyPktSubkey+ _ -> False isSSKPWS _ = False -brokenSecSubkeyWithWireRep ::- Parser [PktWithWireRep] [((Pkt, [SignaturePayload]), [PktWithWireRep])]+brokenSecSubkeyWithWireRep+ :: Parser+ [PktWithWireRep]+ [((Pkt, [SignaturePayload]), [PktWithWireRep])] brokenSecSubkeyWithWireRep = do- _ <- brokenWithWireRep 7- _ <- concatMany (signatureWithWireRepPredicate False isAllowedSubkeySigType)- return []+ _ <- brokenWithWireRep 7+ _ <-+ concatMany+ (signatureWithWireRepPredicate False isAllowedSubkeySigType)+ return [] -skPayloadWithWireRep ::- Parser [PktWithWireRep] ((SomePKPayload, Maybe SKAddendum), [PktWithWireRep])+skPayloadWithWireRep+ :: Parser+ [PktWithWireRep]+ ((SomePKPayload, Maybe SKAddendum), [PktWithWireRep]) skPayloadWithWireRep = do- spkts <- satisfy isSKPWS- case spkts of- [pktWithSource] ->- case pktToSecretKeyPkt (_pktValue pktWithSource) of- Just keyPkt | keyPktRole keyPkt == KeyPktPrimary ->- return (keyPktTKKey keyPkt, [pktWithSource])+ spkts <- satisfy isSKPWS+ case spkts of+ [pktWithSource] ->+ case pktToSecretKeyPkt (_pktValue pktWithSource) of+ Just keyPkt+ | keyPktRole keyPkt == KeyPktPrimary ->+ return (keyPktTKKey keyPkt, [pktWithSource])+ _ -> failure _ -> failure- _ -> failure where isSKPWS [pktWithSource] =- case pktToSecretKeyPkt (_pktValue pktWithSource) of- Just keyPkt -> keyPktRole keyPkt == KeyPktPrimary- _ -> False+ case pktToSecretKeyPkt (_pktValue pktWithSource) of+ Just keyPkt -> keyPktRole keyPkt == KeyPktPrimary+ _ -> False isSKPWS _ = False -brokenWithWireRep :: Int -> Parser [PktWithWireRep] PktWithWireRep+brokenWithWireRep+ :: Int -> Parser [PktWithWireRep] PktWithWireRep brokenWithWireRep t = do- bpkts <- satisfy isBrokenWS- case bpkts of- [bp] -> return bp- _ -> failure+ bpkts <- satisfy isBrokenWS+ case bpkts of+ [bp] -> return bp+ _ -> failure where isBrokenWS [pktWithSource] =- case _pktValue pktWithSource of- BrokenPacketPkt _ a _ -> t == fromIntegral a- _ -> False+ case _pktValue pktWithSource of+ BrokenPacketPkt _ a _ -> t == fromIntegral a+ _ -> False isBrokenWS _ = False -parseTKsWithWireRep :: Bool -> [PktWithWireRep] -> [TKWithWireRep]+parseTKsWithWireRep+ :: Bool -> [PktWithWireRep] -> [TKWithWireRep] parseTKsWithWireRep intolerant ps =- catMaybes $- runIncrementalParser- (anyTKWithWireRep intolerant)- (map (: []) (filter notTrustPacketWithWireRep ps))+ catMaybes $+ runIncrementalParser+ (anyTKWithWireRep intolerant)+ (map (: []) (filter notTrustPacketWithWireRep ps)) where notTrustPacketWithWireRep = not . isTrustPkt . _pktValue -runIncrementalParser ::- (Monoid s, Show s)- => Parser s r- -> [s]- -> [r]+runIncrementalParser+ :: (Monoid s, Show s)+ => Parser s r+ -> [s]+ -> [r] runIncrementalParser parser chunks = go ([], Just (Nothing, parser)) chunks where go st [] = snd (finalizeParsing st)- go st (chunk:rest) =- let (st', out) = parseAChunk parser chunk st- in out <> go st' rest+ go st (chunk : rest) =+ let (st', out) = parseAChunk parser chunk st+ in out <> go st' rest mkTKWithWireRep :: TKUnknown -> [PktWithWireRep] -> TKWithWireRep mkTKWithWireRep tk refs =- case refs of- [] -> error "mkTKWithWireRep requires at least one packet reference"- (pktWithSource:_) ->- TKWithWireRep- (NE.singleton (wireRepOfPkt pktWithSource))- (spanByteRanges (map _pktRange refs))- refs- tk+ case refs of+ [] ->+ error "mkTKWithWireRep requires at least one packet reference"+ (pktWithSource : _) ->+ TKWithWireRep+ (NE.singleton (wireRepOfPkt pktWithSource))+ (spanByteRanges (map _pktRange refs))+ refs+ tk
Codec/Encryption/OpenPGP/Message.hs view
@@ -2,896 +2,1082 @@ -- Copyright © 2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).--{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE StandaloneDeriving #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}--module Codec.Encryption.OpenPGP.Message- ( Passphrase- , mkPassphrase- , passphraseBytes- , EncryptedPayload- , mkEncryptedPayload- , encryptedPayloadBytes- , ClearPayload- , mkClearPayload- , clearPayloadBytes- , WrappedSessionMaterial- , SigningAlgorithm(..)- , SecretKeyFor- , VersionedPKPayload- , asV4PKPayload- , asV6PKPayload- , Signer- , SigningCapability- , mkRSASignerV4- , mkRSASignerV6- , mkEd25519SignerV4- , mkEd25519SignerV6- , mkEd448SignerV4- , mkEd448SignerV6- , MessageParseFailure(..)- , MessageDecryptFailure(..)- , MessageError(..)- , SessionMaterialExposure(..)- , EncryptMessageProfile- , EncryptMessageOptions(..)- , RecoveredSessionMaterial(..)- , encryptMessage- , decryptMessage- , signMessage- , signMessageWith- , ConduitMessage.VerificationPolicy(..)- , ConduitMessage.VerificationOptions(..)- , ConduitMessage.defaultVerificationOptions- , verifySignedMessage- ) where--import Data.Bifunctor (first)-import Data.Kind (Type)-import Control.Monad.Trans.Class (lift)-import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)-import qualified Crypto.Hash as CH-import qualified Crypto.PubKey.Ed25519 as Ed25519-import qualified Crypto.PubKey.Ed448 as Ed448-import qualified Crypto.PubKey.RSA.Types as RSATypes-import qualified Data.ByteArray as BA-import Crypto.Random.Types (MonadRandom, getRandomBytes)-import Data.Binary (put)-import Data.Binary.Put (runPut)-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import Data.Word (Word8)--import Codec.Encryption.OpenPGP.BlockCipher (CipherError, renderCipherError, keySize, withSymmetricCipher)-import Codec.Encryption.OpenPGP.CFB- ( OpenPGPCFBModeW(..)- , decryptOpenPGPCfb- , decryptPreservingNonce- , encryptOpenPGPCfbRaw- , mdcTrailerForSEIPDv1- , seipdv1NonceFromIV- , validateSEIPD1MDC- )-import Codec.Encryption.OpenPGP.Encrypt (encryptSEIPDv2WithSKESKBlock)-import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)-import Codec.Encryption.OpenPGP.Internal.HOBlockCipher (HOBlockCipher(..))-import Codec.Encryption.OpenPGP.Internal.CryptoSEIPDv2- ( decryptSKESK6SessionKey- , deriveSKESK6KEK- )-import Codec.Encryption.OpenPGP.Policy- ( OpenPGPPolicy- , defaultPolicy- , deprecatedHashAlgorithms- , messageDefaultAEADAlgorithm- , messageDefaultChunkSize- , messageSEIPDv2SaltOctets- , policyGenerationDeprecations- , policyMessageEncryption- , supportsSEIPDv2Symmetric- , OpenPGPRFCW(..)- , HashAlgorithmW(..)- )-import Codec.Encryption.OpenPGP.S2K (S2KError(..), renderS2KError, skesk2SessionKey, string2Key)-import Codec.Encryption.OpenPGP.Serialize (parsePkts)-import Codec.Encryption.OpenPGP.Signatures- ( SignError(..)- , VerificationError- , signDataWithRSABuilder- , signDataWithRSAV6Builder- , signDataWithEd25519Builder- , signDataWithEd25519V6Builder- , signDataWithEd448Builder- , signDataWithEd448V6Builder- )-import Codec.Encryption.OpenPGP.Subpackets- ( sigBuilderInitTyped- , sigBuilderInitV6Typed- , addHashedSubs- , addUnhashedSubs- , listToHashedSubs- , listToUnhashedSubs- )-import Codec.Encryption.OpenPGP.Types-import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as PKA-import Data.Conduit.OpenPGP.Decrypt (decryptSEIPDv2Payload)-import qualified Data.Conduit.OpenPGP.Message as ConduitMessage--newtype Passphrase = Passphrase { unPassphrase :: BL.ByteString }- deriving (Eq, Ord, Show)--newtype EncryptedPayload = EncryptedPayload { unEncryptedPayload :: BL.ByteString }- deriving (Eq, Ord, Show)--newtype ClearPayload = ClearPayload { unClearPayload :: BL.ByteString }- deriving (Eq, Ord, Show)--newtype WrappedSessionMaterial = WrappedSessionMaterial { unWrappedSessionMaterial :: B.ByteString }- deriving (Eq, Ord, Show)--data SigningAlgorithm = AlgoRSA | AlgoEd25519 | AlgoEd448--type family SecretKeyFor (alg :: SigningAlgorithm) where- SecretKeyFor 'AlgoRSA = RSATypes.PrivateKey- SecretKeyFor 'AlgoEd25519 = Ed25519.SecretKey- SecretKeyFor 'AlgoEd448 = Ed448.SecretKey--type family KeyVersionForSig (v :: Type) :: KeyVersion where- KeyVersionForSig V4Sig = 'V4- KeyVersionForSig V6Sig = 'V6--data VersionedPKPayload (v :: KeyVersion) where- VersionedPKPayloadV4 :: PKPayload 'V4 -> VersionedPKPayload 'V4- VersionedPKPayloadV6 :: PKPayload 'V6 -> VersionedPKPayload 'V6--asV4PKPayload :: SomePKPayload -> Either String (VersionedPKPayload 'V4)-asV4PKPayload (SomePKPayload pk@(PKPayloadV4 _ _ _)) =- Right (VersionedPKPayloadV4 pk)-asV4PKPayload _ = Left "Expected a v4 PKPayload"--asV6PKPayload :: SomePKPayload -> Either String (VersionedPKPayload 'V6)-asV6PKPayload (SomePKPayload pk@(PKPayloadV6 _ _ _)) =- Right (VersionedPKPayloadV6 pk)-asV6PKPayload _ = Left "Expected a v6 PKPayload"--data Signer (alg :: SigningAlgorithm) (v :: Type) where- RSASigner :: VersionedPKPayload (KeyVersionForSig v) -> SecretKeyFor 'AlgoRSA -> Signer 'AlgoRSA v- Ed25519Signer :: VersionedPKPayload (KeyVersionForSig v) -> SecretKeyFor 'AlgoEd25519 -> Signer 'AlgoEd25519 v- Ed448Signer :: VersionedPKPayload (KeyVersionForSig v) -> SecretKeyFor 'AlgoEd448 -> Signer 'AlgoEd448 v--class SigningCapability (alg :: SigningAlgorithm) v-instance SigningCapability 'AlgoRSA V4Sig-instance SigningCapability 'AlgoRSA V6Sig-instance SigningCapability 'AlgoEd25519 V4Sig-instance SigningCapability 'AlgoEd25519 V6Sig-instance SigningCapability 'AlgoEd448 V4Sig-instance SigningCapability 'AlgoEd448 V6Sig--mkRSASignerV4 :: VersionedPKPayload 'V4 -> SecretKeyFor 'AlgoRSA -> Signer 'AlgoRSA V4Sig-mkRSASignerV4 = RSASigner--mkRSASignerV6 :: VersionedPKPayload 'V6 -> SecretKeyFor 'AlgoRSA -> Signer 'AlgoRSA V6Sig-mkRSASignerV6 = RSASigner--mkEd25519SignerV4 :: VersionedPKPayload 'V4 -> SecretKeyFor 'AlgoEd25519 -> Signer 'AlgoEd25519 V4Sig-mkEd25519SignerV4 = Ed25519Signer--mkEd25519SignerV6 :: VersionedPKPayload 'V6 -> SecretKeyFor 'AlgoEd25519 -> Signer 'AlgoEd25519 V6Sig-mkEd25519SignerV6 = Ed25519Signer--mkEd448SignerV4 :: VersionedPKPayload 'V4 -> SecretKeyFor 'AlgoEd448 -> Signer 'AlgoEd448 V4Sig-mkEd448SignerV4 = Ed448Signer--mkEd448SignerV6 :: VersionedPKPayload 'V6 -> SecretKeyFor 'AlgoEd448 -> Signer 'AlgoEd448 V6Sig-mkEd448SignerV6 = Ed448Signer--data MessageError- = MessageEncryptError String- | MessageDecryptError String- | MessageSignError SignError- | MessageParseError String- | MessageParseFailureError MessageParseFailure- | MessageDecryptFailureError MessageDecryptFailure- deriving (Eq, Show)--newtype MessageFlow a =- MessageFlow- { runMessageFlow :: Either MessageError a- }--instance Functor MessageFlow where- fmap f (MessageFlow result) = MessageFlow (fmap f result)--instance Applicative MessageFlow where- pure = MessageFlow . Right- MessageFlow ff <*> MessageFlow fa = MessageFlow (ff <*> fa)--instance Monad MessageFlow where- MessageFlow result >>= f =- case result of- Left err -> MessageFlow (Left err)- Right x -> f x--messageStep :: Either MessageError a -> MessageFlow a-messageStep = MessageFlow--type MessageFlowT m = ExceptT MessageError m--runMessageFlowT :: MessageFlowT m a -> m (Either MessageError a)-runMessageFlowT = runExceptT--liftMessageFlowT :: Monad m => MessageFlow a -> MessageFlowT m a-liftMessageFlowT (MessageFlow result) =- case result of- Left err -> throwE err- Right x -> pure x--data MessageParseFailure- = MissingEncryptedMessage- | ExpectedSKESKThenEncryptedData- | SKESKSEIPDAlgorithmMismatch- | UnsupportedEncryptedSKESK- | MissingLiteralDataPacket- | UnknownCriticalPacketType Word8- | BrokenCriticalPacketType Word8 String- deriving (Eq, Show)--data MessageDecryptFailure- = SessionMaterialDerivationFailed S2KError- | PayloadDecryptFailed String- deriving (Eq, Show)--data ParsedEncryptedPayloadKind- = LegacySEDPayloadKind- | LegacySEIPDv1PayloadKind- | SEIPDv2PayloadKind--data EncryptedPreludeKind- = LegacyEncryptedPreludeKind- | SEIPDv2EncryptedPreludeKind--data SessionMaterialExposure- = DoNotExposeSessionMaterial- | ExposeSessionMaterial- deriving (Eq, Show)--data EncryptMessageProfile- = RFC4880Message- | RFC9580Message--data EncryptMessageOptions (p :: EncryptMessageProfile) where- RFC4880EncryptMessageOptions ::- { rfc4880EncryptMessageExposure :: SessionMaterialExposure- , rfc4880EncryptMessageSymmetricAlgorithm :: SymmetricAlgorithm- , rfc4880EncryptMessageS2K :: S2K- , rfc4880EncryptMessageIV :: IV- }- -> EncryptMessageOptions 'RFC4880Message- RFC9580EncryptMessageOptions ::- { rfc9580EncryptMessageExposure :: SessionMaterialExposure- , rfc9580EncryptMessageSymmetricAlgorithm :: SymmetricAlgorithm- , rfc9580EncryptMessageS2K :: S2K- , rfc9580EncryptMessageIV :: IV- }- -> EncryptMessageOptions 'RFC9580Message--deriving instance Eq (EncryptMessageOptions p)-deriving instance Show (EncryptMessageOptions p)--data RecoveredSessionMaterial =- RecoveredSessionMaterial- { recoveredSessionAlgorithm :: SymmetricAlgorithm- , recoveredSessionKey :: SessionKey- }- deriving (Eq, Show)--renderMessageParseFailure :: MessageParseFailure -> String-renderMessageParseFailure MissingEncryptedMessage =- "Could not parse encrypted OpenPGP message"-renderMessageParseFailure ExpectedSKESKThenEncryptedData =- "Expected an SKESK packet followed by symmetrically encrypted data or SEIPD v2 data"-renderMessageParseFailure SKESKSEIPDAlgorithmMismatch =- "SKESK and SEIPD v2 algorithms do not match"-renderMessageParseFailure UnsupportedEncryptedSKESK =- "Cannot decrypt SKESK packets with encrypted session keys"-renderMessageParseFailure MissingLiteralDataPacket =- "Decrypted message does not contain a literal data packet"-renderMessageParseFailure (UnknownCriticalPacketType t) =- "Unknown critical packet type: " ++ show t-renderMessageParseFailure (BrokenCriticalPacketType t err) =- "Broken critical packet type " ++ show t ++ ": " ++ err--renderMessageDecryptFailure :: MessageDecryptFailure -> String-renderMessageDecryptFailure (SessionMaterialDerivationFailed err) = renderS2KError err-renderMessageDecryptFailure (PayloadDecryptFailed err) = err--mkPassphrase :: BL.ByteString -> Passphrase-mkPassphrase = Passphrase--passphraseBytes :: Passphrase -> BL.ByteString-passphraseBytes = unPassphrase--mkEncryptedPayload :: BL.ByteString -> EncryptedPayload-mkEncryptedPayload = EncryptedPayload--mkClearPayload :: BL.ByteString -> ClearPayload-mkClearPayload = ClearPayload--clearPayloadBytes :: ClearPayload -> BL.ByteString-clearPayloadBytes = unClearPayload--encryptedPayloadBytes :: EncryptedPayload -> BL.ByteString-encryptedPayloadBytes = unEncryptedPayload--firstLeft :: (e -> e') -> Either e a -> Either e' a-firstLeft f = either (Left . f) Right---- | Lift a parse failure step into the unified MessageError channel-parseStep :: Either MessageParseFailure a -> MessageFlow a-parseStep = messageStep . firstLeft MessageParseFailureError---- | Lift a decrypt failure step into the unified MessageError channel-decryptStep :: Either MessageDecryptFailure a -> MessageFlow a-decryptStep = messageStep . firstLeft MessageDecryptFailureError---- | Lift a string encrypt error step into the unified MessageError channel-encryptStep :: Either String a -> MessageFlow a-encryptStep = messageStep . firstLeft MessageEncryptError---- | Lift a sign error step into the unified MessageError channel-signStep :: Either SignError a -> MessageFlow a-signStep = messageStep . firstLeft MessageSignError--signStepT :: Monad m => Either SignError a -> MessageFlowT m a-signStepT = liftMessageFlowT . signStep--signBackendStep :: Either String a -> Either SignError a-signBackendStep = firstLeft SignBackendError--decryptSessionStep :: Either S2KError a -> Either MessageDecryptFailure a-decryptSessionStep = firstLeft SessionMaterialDerivationFailed--decryptSessionKeySizeStep :: Either CipherError a -> Either MessageDecryptFailure a-decryptSessionKeySizeStep =- firstLeft (SessionMaterialDerivationFailed . S2KUnsupportedAlgorithm)--decryptCipherStep :: Either CipherError a -> Either MessageDecryptFailure a-decryptCipherStep = firstLeft (PayloadDecryptFailed . renderCipherError)--decryptPayloadStep :: Either String a -> Either MessageDecryptFailure a-decryptPayloadStep = firstLeft PayloadDecryptFailed--encryptMessage ::- EncryptMessageOptions p- -> Passphrase- -> ClearPayload- -> Either MessageError (EncryptedPayload, Maybe RecoveredSessionMaterial)-encryptMessage options passphrase payload = runMessageFlow $- case options of- RFC4880EncryptMessageOptions exposure sa s2k iv -> do- encryptedPayload <- encryptMessageWithRFC4880Fallback sa s2k iv passphrase payload- sessionKeyMaterial <- deriveSessionMaterial sa s2k passphrase- pure (encryptedPayload, exposedSessionMaterial exposure sa sessionKeyMaterial)- RFC9580EncryptMessageOptions exposure sa s2k iv -> do- encryptStep $ validateRFC9580MessageSymmetric defaultPolicy sa- encryptStep $ validateModernMessageS2K defaultPolicy s2k- encrypted <-- encryptStep $- encryptSEIPDv2WithSKESKBlock- sa- (messageDefaultAEADAlgorithm messagePolicy)- (messageDefaultChunkSize messagePolicy)- (defaultSEIPDv2SaltFromIV (messageSEIPDv2SaltOctets messagePolicy) iv)- s2k- (unPassphrase passphrase)- (Block [LiteralDataPkt BinaryData BL.empty 0 (unClearPayload payload)])- sessionKeyMaterial <- deriveSessionMaterial sa s2k passphrase- pure- ( EncryptedPayload (runPut (put (Block encrypted)))- , exposedSessionMaterial exposure sa sessionKeyMaterial- )- where- messagePolicy = policyMessageEncryption defaultPolicy--defaultSEIPDv2SaltFromIV :: Int -> IV -> Salt-defaultSEIPDv2SaltFromIV outputLen (IV ivBytes) =- Salt (B.take outputLen (B.concat (replicate outputLen seed)))- where- seed- | B.null ivBytes = B.singleton 0- | otherwise = ivBytes--encryptMessageWithRFC4880Fallback ::- SymmetricAlgorithm- -> S2K- -> IV- -> Passphrase- -> ClearPayload- -> MessageFlow EncryptedPayload-encryptMessageWithRFC4880Fallback sa s2k iv passphrase payload = do- keyLen <- encryptStep . first renderCipherError $ keySize sa- sessionMaterial <- encryptStep . first renderS2KError $- WrappedSessionMaterial <$> string2Key s2k keyLen (unPassphrase passphrase)- let literal = LiteralDataPkt BinaryData BL.empty 0 (unClearPayload payload)- cleartext = BL.toStrict (runPut (put (Block [literal])))- cleartextWithMDC = cleartext <> mdcTrailerForSEIPDv1 iv cleartext- encrypted <- encryptStep . first renderCipherError $- encryptOpenPGPCfbRaw OpenPGPCFBNoResyncW sa iv cleartextWithMDC (unWrappedSessionMaterial sessionMaterial)- return . EncryptedPayload . runPut . put $- Block- [ SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k Nothing))- , SymEncIntegrityProtectedDataPkt (SEIPD1 1 (BL.fromStrict encrypted))- ]--deriveSessionMaterial ::- SymmetricAlgorithm -> S2K -> Passphrase -> MessageFlow B.ByteString-deriveSessionMaterial sa s2k passphrase = do- keyLen <- encryptStep . first renderCipherError $ keySize sa- encryptStep . first renderS2KError $ string2Key s2k keyLen (unPassphrase passphrase)--exposedSessionMaterial ::- SessionMaterialExposure- -> SymmetricAlgorithm- -> B.ByteString- -> Maybe RecoveredSessionMaterial-exposedSessionMaterial DoNotExposeSessionMaterial _ _ = Nothing-exposedSessionMaterial ExposeSessionMaterial sa sessionKeyMaterial =- Just- (RecoveredSessionMaterial- { recoveredSessionAlgorithm = sa- , recoveredSessionKey = SessionKey sessionKeyMaterial- })--decryptMessage :: Passphrase -> EncryptedPayload -> Either MessageError ClearPayload-decryptMessage passphrase encrypted = runMessageFlow $ do- encryptedPackets <- parseStep $ rejectUnknownCriticalPacketsTyped (parsePkts (unEncryptedPayload encrypted))- payload <- parseStep $ extractEncryptedPayload encryptedPackets- cleartext <- decryptStep $ decryptPayloadTyped passphrase payload- clearPackets <- parseStep $ rejectUnknownCriticalPacketsTyped (parsePkts (unClearPayload cleartext))- parseStep $ extractLiteralPayload clearPackets--signMessageWith ::- (MonadRandom m, SigningCapability alg v)- => Signer alg v- -> ClearPayload- -> m (Either MessageError BL.ByteString)-signMessageWith signer payload = runMessageFlowT $- case signer of- RSASigner signerPK signingKey ->- case signerPK of- VersionedPKPayloadV4 pk ->- signV4Message- pk- (\hashed unhashed clear ->- let builder =- sigBuilderInitTyped @'PKA.RSA RFC9580W BinarySig SHA512W- withHashed = addHashedSubs (listToHashedSubs hashed) builder- withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed- in signDataWithRSABuilder withUnhashed signingKey clear)- payload- VersionedPKPayloadV6 pk ->- signV6Message- pk- (\salt hashed unhashed clear ->- let builder =- sigBuilderInitV6Typed @'PKA.RSA RFC9580W BinarySig SHA512W salt- withHashed = addHashedSubs (listToHashedSubs hashed) builder- withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed- in signDataWithRSAV6Builder withUnhashed signingKey clear)- payload- Ed25519Signer signerPK signingKey ->- case signerPK of- VersionedPKPayloadV4 pk ->- signV4Message- pk- (\hashed unhashed clear ->- let builder =- sigBuilderInitTyped @'PKA.Ed25519 RFC9580W BinarySig SHA512W- withHashed = addHashedSubs (listToHashedSubs hashed) builder- withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed- in signDataWithEd25519Builder withUnhashed signingKey clear)- payload- VersionedPKPayloadV6 pk ->- signV6Message- pk- (\salt hashed unhashed clear ->- let builder =- sigBuilderInitV6Typed @'PKA.Ed25519 RFC9580W BinarySig SHA512W salt- withHashed = addHashedSubs (listToHashedSubs hashed) builder- withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed- in signDataWithEd25519V6Builder withUnhashed signingKey clear)- payload- Ed448Signer signerPK signingKey ->- case signerPK of- VersionedPKPayloadV4 pk ->- signV4Message- pk- (\hashed unhashed clear ->- let builder =- sigBuilderInitTyped @'PKA.Ed448 RFC9580W BinarySig SHA512W- withHashed = addHashedSubs (listToHashedSubs hashed) builder- withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed- in signDataWithEd448Builder withUnhashed signingKey clear)- payload- VersionedPKPayloadV6 pk ->- signV6Message- pk- (\salt hashed unhashed clear ->- let builder =- sigBuilderInitV6Typed @'PKA.Ed448 RFC9580W BinarySig SHA512W salt- withHashed = addHashedSubs (listToHashedSubs hashed) builder- withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed- in signDataWithEd448V6Builder withUnhashed signingKey clear)- payload--signMessage ::- (MonadRandom m, SigningCapability alg v)- => Signer alg v- -> BL.ByteString- -> m (Either MessageError BL.ByteString)-signMessage signer = signMessageWith signer . mkClearPayload--versionedPKPayload :: VersionedPKPayload v -> PKPayload v-versionedPKPayload (VersionedPKPayloadV4 pk) = pk-versionedPKPayload (VersionedPKPayloadV6 pk) = pk--verifySignedMessage ::- ConduitMessage.VerificationOptions- -> PublicKeyring- -> BL.ByteString- -> [Either VerificationError Verification]-verifySignedMessage = ConduitMessage.verifyMessage--signV4Message ::- Monad m- => PKPayload 'V4- -> ([SigSubPacket] -> [SigSubPacket] -> BL.ByteString -> Either SignError SignaturePayload)- -> ClearPayload- -> MessageFlowT m BL.ByteString-signV4Message signer signingFn payload =- signStepT (signV4WithIssuers signer signingFn payload)--signV6Message ::- MonadRandom m- => PKPayload 'V6- -> (SignatureSalt -> [SigSubPacket] -> [SigSubPacket] -> BL.ByteString -> Either SignError SignaturePayload)- -> ClearPayload- -> MessageFlowT m BL.ByteString-signV6Message signer signingFn payload = do- salt <- lift randomSHA512SignatureSalt- signStepT (signV6WithFingerprintOnly signer (signingFn salt) payload)--randomSHA512SignatureSalt :: MonadRandom m => m SignatureSalt-randomSHA512SignatureSalt =- SignatureSalt . BL.fromStrict <$> getRandomBytes 32--signV4WithIssuers ::- PKPayload 'V4- -> ([SigSubPacket] -> [SigSubPacket] -> BL.ByteString -> Either SignError SignaturePayload)- -> ClearPayload- -> Either SignError BL.ByteString-signV4WithIssuers signer signingFn payload = do- issuerKeyId <- signBackendStep (eightOctetKeyID (SomePKPayload signer))- let hashed = [SigSubPacket False (IssuerFingerprint IssuerFingerprintV4 (fingerprint (SomePKPayload signer)))]- unhashed = [SigSubPacket False (Issuer issuerKeyId)]- signWithSubpackets hashed unhashed signingFn payload--signV6WithFingerprintOnly ::- PKPayload 'V6- -> ([SigSubPacket] -> [SigSubPacket] -> BL.ByteString -> Either SignError SignaturePayload)- -> ClearPayload- -> Either SignError BL.ByteString-signV6WithFingerprintOnly signer signingFn payload = do- let hashed = [SigSubPacket False (IssuerFingerprint IssuerFingerprintV6 (fingerprint (SomePKPayload signer)))]- unhashed = []- signWithSubpackets hashed unhashed signingFn payload--signWithSubpackets ::- [SigSubPacket]- -> [SigSubPacket]- -> ([SigSubPacket] -> [SigSubPacket] -> BL.ByteString -> Either SignError SignaturePayload)- -> ClearPayload- -> Either SignError BL.ByteString-signWithSubpackets hashed unhashed signingFn payload = do- let clear = unClearPayload payload- literal = LiteralDataPkt BinaryData BL.empty 0 clear- signature <- signingFn hashed unhashed clear- return . runPut . put $ Block [literal, SignaturePkt signature]--encryptOpenPGPCfb ::- SymmetricAlgorithm- -> IV- -> B.ByteString- -> WrappedSessionMaterial- -> Either CipherError B.ByteString-encryptOpenPGPCfb sa iv cleartext (WrappedSessionMaterial keydata) =- encryptOpenPGPCfbRaw OpenPGPCFBResyncW sa iv cleartext keydata--extractEncryptedPayload ::- [Pkt] -> Either MessageParseFailure SomeParsedEncryptedPayload-extractEncryptedPayload =- fmap parsedEncryptedPayloadFromPrelude . extractEncryptedPreludeTyped--data EncryptedPrelude (k :: EncryptedPreludeKind) where- LegacySEDPrelude ::- SKESK 'SKESKV4- -> B.ByteString- -> EncryptedPrelude 'LegacyEncryptedPreludeKind- LegacySEIPDv1Prelude ::- SKESK 'SKESKV4- -> B.ByteString- -> EncryptedPrelude 'LegacyEncryptedPreludeKind- SEIPDv2SKESK4Prelude ::- SymmetricAlgorithm- -> S2K- -> AEADAlgorithm- -> Word8- -> Salt- -> B.ByteString- -> EncryptedPrelude 'SEIPDv2EncryptedPreludeKind- SEIPDv2SKESK6Prelude ::- SymmetricAlgorithm- -> AEADAlgorithm- -> S2K- -> BL.ByteString- -> BL.ByteString- -> BL.ByteString- -> Word8- -> Salt- -> B.ByteString- -> EncryptedPrelude 'SEIPDv2EncryptedPreludeKind--data SomeEncryptedPrelude where- SomeEncryptedPrelude :: EncryptedPrelude k -> SomeEncryptedPrelude--extractEncryptedPreludeTyped ::- [Pkt] -> Either MessageParseFailure SomeEncryptedPrelude-extractEncryptedPreludeTyped (SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k esk)):SymEncDataPkt payload:_) =- Right- (SomeEncryptedPrelude- (LegacySEDPrelude- (SKESK4Packet sa s2k esk)- (BL.toStrict payload)))-extractEncryptedPreludeTyped (SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k esk)):SymEncIntegrityProtectedDataPkt (SEIPD1 _ payload):_) =- Right- (SomeEncryptedPrelude- (LegacySEIPDv1Prelude- (SKESK4Packet sa s2k esk)- (BL.toStrict payload)))-extractEncryptedPreludeTyped (SKESKPkt skesk:SymEncIntegrityProtectedDataPkt (SEIPD2 payloadSA aead chunkSize salt payload):_) =- toSEIPDv2Prelude skesk payloadSA aead chunkSize salt payload-extractEncryptedPreludeTyped [] = Left MissingEncryptedMessage-extractEncryptedPreludeTyped _ = Left ExpectedSKESKThenEncryptedData--toSEIPDv2Prelude ::- SKESKPayload- -> SymmetricAlgorithm- -> AEADAlgorithm- -> Word8- -> Salt- -> BL.ByteString- -> Either MessageParseFailure SomeEncryptedPrelude-toSEIPDv2Prelude (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k Nothing)) payloadSA aead chunkSize salt payload- | sa /= payloadSA = Left SKESKSEIPDAlgorithmMismatch- | otherwise =- Right- (SomeEncryptedPrelude- (SEIPDv2SKESK4Prelude- sa- s2k- aead- chunkSize- salt- (BL.toStrict payload)))-toSEIPDv2Prelude (SKESKPayloadV6Packet (SKESKPayloadV6 sa aa s2k iv esk tag)) payloadSA aead chunkSize salt payload- | sa /= payloadSA = Left SKESKSEIPDAlgorithmMismatch- | otherwise =- Right- (SomeEncryptedPrelude- (SEIPDv2SKESK6Prelude- sa- aa- s2k- iv- esk- tag- chunkSize- salt- (BL.toStrict payload)))-toSEIPDv2Prelude (SKESKPayloadV4Packet (SKESKPayloadV4 _ _ (Just _))) _ _ _ _ _ = Left UnsupportedEncryptedSKESK-toSEIPDv2Prelude _ _ _ _ _ _ = Left ExpectedSKESKThenEncryptedData--parsedEncryptedPayloadFromPrelude :: SomeEncryptedPrelude -> SomeParsedEncryptedPayload-parsedEncryptedPayloadFromPrelude (SomeEncryptedPrelude prelude) =- case prelude of- LegacySEDPrelude skesk payload ->- SomeParsedEncryptedPayload (LegacySEDPayload skesk payload)- LegacySEIPDv1Prelude skesk payload ->- SomeParsedEncryptedPayload (LegacySEIPDv1Payload skesk payload)- SEIPDv2SKESK4Prelude sa s2k aead chunkSize salt payload ->- SomeParsedEncryptedPayload- (SEIPDv2Payload- sa- aead- chunkSize- salt- (SEIPDv2SKESK4 sa s2k)- payload)- SEIPDv2SKESK6Prelude sa aa s2k iv esk tag chunkSize salt payload ->- SomeParsedEncryptedPayload- (SEIPDv2Payload- sa- aa- chunkSize- salt- (SEIPDv2SKESK6 sa aa s2k iv esk tag)- payload)--data SEIPDv2SKESKInfo (v :: KeyVersion) where- SEIPDv2SKESK4 :: SymmetricAlgorithm -> S2K -> SEIPDv2SKESKInfo 'V4- SEIPDv2SKESK6 ::- SymmetricAlgorithm- -> AEADAlgorithm- -> S2K- -> BL.ByteString- -> BL.ByteString- -> BL.ByteString- -> SEIPDv2SKESKInfo 'V6--data ParsedEncryptedPayload (k :: ParsedEncryptedPayloadKind) where- LegacySEDPayload ::- SKESK 'SKESKV4- -> B.ByteString- -> ParsedEncryptedPayload 'LegacySEDPayloadKind- LegacySEIPDv1Payload ::- SKESK 'SKESKV4- -> B.ByteString- -> ParsedEncryptedPayload 'LegacySEIPDv1PayloadKind- SEIPDv2Payload ::- SymmetricAlgorithm- -> AEADAlgorithm- -> Word8- -> Salt- -> SEIPDv2SKESKInfo v- -> B.ByteString- -> ParsedEncryptedPayload 'SEIPDv2PayloadKind--data SomeParsedEncryptedPayload where- SomeParsedEncryptedPayload ::- ParsedEncryptedPayload k- -> SomeParsedEncryptedPayload--decryptPayload :: Passphrase -> SomeParsedEncryptedPayload -> Either String ClearPayload-decryptPayload passphrase =- first renderMessageDecryptFailure . decryptPayloadTyped passphrase--decryptPayloadTyped ::- Passphrase -> SomeParsedEncryptedPayload -> Either MessageDecryptFailure ClearPayload-decryptPayloadTyped passphrase (SomeParsedEncryptedPayload payload) =- case payload of- LegacySEDPayload skesk encryptedPayload ->- decryptLegacySEDPayloadTyped passphrase skesk encryptedPayload- LegacySEIPDv1Payload skesk encryptedPayload ->- decryptLegacySEIPDv1PayloadTyped passphrase skesk encryptedPayload- SEIPDv2Payload sa aead chunkSize salt skeskInfo encryptedPayload ->- decryptSEIPDv2PayloadTyped- passphrase- sa- aead- chunkSize- salt- skeskInfo- encryptedPayload--decryptLegacySEDPayloadTyped ::- Passphrase- -> SKESK 'SKESKV4- -> B.ByteString- -> Either MessageDecryptFailure ClearPayload-decryptLegacySEDPayloadTyped passphrase skesk payload = do- (sessionAlgorithm, sessionKeyBytes) <-- decryptSessionStep $- skesk2SessionKey skesk (unPassphrase passphrase)- decryptCipherStep $- ClearPayload . BL.fromStrict <$>- decryptOpenPGPCfb- sessionAlgorithm- payload- sessionKeyBytes--decryptLegacySEIPDv1PayloadTyped ::- Passphrase- -> SKESK 'SKESKV4- -> B.ByteString- -> Either MessageDecryptFailure ClearPayload-decryptLegacySEIPDv1PayloadTyped passphrase skesk payload = do- (sessionAlgorithm, sessionKeyBytes) <-- decryptSessionStep $- skesk2SessionKey skesk (unPassphrase passphrase)- (nonce, decrypted) <-- decryptCipherStep $- decryptPreservingNonce sessionAlgorithm payload sessionKeyBytes- cleartext <- decryptPayloadStep $ validateSEIPD1MDC nonce decrypted- Right (ClearPayload (BL.fromStrict cleartext))--decryptSEIPDv2PayloadTyped ::- Passphrase- -> SymmetricAlgorithm- -> AEADAlgorithm- -> Word8- -> Salt- -> SEIPDv2SKESKInfo v- -> B.ByteString- -> Either MessageDecryptFailure ClearPayload-decryptSEIPDv2PayloadTyped passphrase sa aead chunkSize salt skeskInfo payload = do- sessionKey <- SessionKey <$> deriveSEIPDv2SessionKeyBytes passphrase skeskInfo- decryptPayloadStep $- ClearPayload . BL.fromStrict <$>- decryptSEIPDv2Payload sa aead chunkSize salt payload sessionKey--deriveSEIPDv2SessionKeyBytes ::- Passphrase- -> SEIPDv2SKESKInfo v- -> Either MessageDecryptFailure B.ByteString-deriveSEIPDv2SessionKeyBytes passphrase (SEIPDv2SKESK4 sa s2k) =- deriveSessionKeyBytes passphrase sa s2k-deriveSEIPDv2SessionKeyBytes passphrase (SEIPDv2SKESK6 sa aead s2k iv esk tag) = do- ikm <- deriveSessionKeyBytes passphrase sa s2k- kek <- decryptPayloadStep $ deriveSKESK6KEK sa aead ikm- decryptPayloadStep $- decryptSKESK6SessionKey sa aead kek (BL.toStrict iv) (BL.toStrict esk) (BL.toStrict tag)--deriveSessionKeyBytes ::- Passphrase- -> SymmetricAlgorithm- -> S2K- -> Either MessageDecryptFailure B.ByteString-deriveSessionKeyBytes passphrase sa s2k = do- keyLen <- decryptSessionKeySizeStep $ keySize sa- decryptSessionStep $ string2Key s2k keyLen (unPassphrase passphrase)--extractLiteralPayload :: [Pkt] -> Either MessageParseFailure ClearPayload-extractLiteralPayload pkts =- case [p | LiteralDataPkt _ _ _ p <- pkts] of- payload:_ -> Right (ClearPayload payload)- [] -> Left MissingLiteralDataPacket--rejectUnknownCriticalPacketsTyped :: [Pkt] -> Either MessageParseFailure [Pkt]-rejectUnknownCriticalPacketsTyped =- go []- where- go acc [] = Right (reverse acc)- go acc (pkt:rest) =- case pkt of- OtherPacketPkt t _ | t < 40 -> Left (UnknownCriticalPacketType t)- BrokenPacketPkt err t _ | t < 40 -> Left (BrokenCriticalPacketType t err)- _ -> go (pkt : acc) rest--validateModernMessageS2K :: OpenPGPPolicy -> S2K -> Either String ()-validateModernMessageS2K policy s2k =- case s2kHashAlgorithm s2k of- Just ha- | ha `elem` deprecatedHashAlgorithms (policyGenerationDeprecations policy) ->- Left- ("deprecated hash algorithm disallowed for modern message generation: " ++- show ha)- _ -> Right ()--validateRFC9580MessageSymmetric ::- OpenPGPPolicy -> SymmetricAlgorithm -> Either String ()-validateRFC9580MessageSymmetric policy sa- | supportsSEIPDv2Symmetric policy sa = Right ()- | otherwise =- Left- ("symmetric algorithm disallowed for RFC9580 message generation: " ++- show sa)+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}++module Codec.Encryption.OpenPGP.Message+ ( Passphrase+ , mkPassphrase+ , passphraseBytes+ , EncryptedPayload+ , mkEncryptedPayload+ , encryptedPayloadBytes+ , ClearPayload+ , mkClearPayload+ , clearPayloadBytes+ , WrappedSessionMaterial+ , SigningAlgorithm (..)+ , SecretKeyFor+ , VersionedPKPayload+ , asV4PKPayload+ , asV6PKPayload+ , Signer+ , SigningCapability+ , mkRSASignerV4+ , mkRSASignerV6+ , mkEd25519SignerV4+ , mkEd25519SignerV6+ , mkEd448SignerV4+ , mkEd448SignerV6+ , MessageParseFailure (..)+ , MessageDecryptFailure (..)+ , MessageError (..)+ , SessionMaterialExposure (..)+ , EncryptMessageProfile+ , EncryptMessageOptions (..)+ , RecoveredSessionMaterial (..)+ , encryptMessage+ , decryptMessage+ , signMessage+ , signMessageWith+ , ConduitMessage.VerificationPolicy (..)+ , ConduitMessage.VerificationOptions (..)+ , ConduitMessage.defaultVerificationOptions+ , verifySignedMessage+ ) where++import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)+import qualified Crypto.Hash as CH+import qualified Crypto.PubKey.Ed25519 as Ed25519+import qualified Crypto.PubKey.Ed448 as Ed448+import qualified Crypto.PubKey.RSA.Types as RSATypes+import Crypto.Random.Types (MonadRandom, getRandomBytes)+import Data.Bifunctor (first)+import Data.Binary (put)+import Data.Binary.Put (runPut)+import qualified Data.ByteArray as BA+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import Data.Kind (Type)+import Data.Word (Word8)++import Codec.Encryption.OpenPGP.BlockCipher+ ( CipherError+ , keySize+ , renderCipherError+ , withSymmetricCipher+ )+import Codec.Encryption.OpenPGP.CFB+ ( OpenPGPCFBModeW (..)+ , decryptOpenPGPCfb+ , decryptPreservingNonce+ , encryptOpenPGPCfbRaw+ , mdcTrailerForSEIPDv1+ , seipdv1NonceFromIV+ , validateSEIPD1MDC+ )+import Codec.Encryption.OpenPGP.Encrypt+ ( encryptSEIPDv2WithSKESKBlock+ )+import Codec.Encryption.OpenPGP.Fingerprint+ ( eightOctetKeyID+ , fingerprint+ )+import Codec.Encryption.OpenPGP.Internal.CryptoSEIPDv2+ ( decryptSKESK6SessionKey+ , deriveSKESK6KEK+ )+import Codec.Encryption.OpenPGP.Internal.HOBlockCipher+ ( HOBlockCipher (..)+ )+import Codec.Encryption.OpenPGP.Policy+ ( HashAlgorithmW (..)+ , OpenPGPPolicy+ , OpenPGPRFCW (..)+ , defaultPolicy+ , deprecatedHashAlgorithms+ , messageDefaultAEADAlgorithm+ , messageDefaultChunkSize+ , messageSEIPDv2SaltOctets+ , policyGenerationDeprecations+ , policyMessageEncryption+ , supportsSEIPDv2Symmetric+ )+import Codec.Encryption.OpenPGP.S2K+ ( S2KError (..)+ , renderS2KError+ , skesk2SessionKey+ , string2Key+ )+import Codec.Encryption.OpenPGP.Serialize (parsePkts)+import Codec.Encryption.OpenPGP.Signatures+ ( SignError (..)+ , VerificationError+ , signDataWithEd25519Builder+ , signDataWithEd25519V6Builder+ , signDataWithEd448Builder+ , signDataWithEd448V6Builder+ , signDataWithRSABuilder+ , signDataWithRSAV6Builder+ )+import Codec.Encryption.OpenPGP.Subpackets+ ( addHashedSubs+ , addUnhashedSubs+ , listToHashedSubs+ , listToUnhashedSubs+ , sigBuilderInitTyped+ , sigBuilderInitV6Typed+ )+import Codec.Encryption.OpenPGP.Types+import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as PKA+import Data.Conduit.OpenPGP.Decrypt (decryptSEIPDv2Payload)+import qualified Data.Conduit.OpenPGP.Message as ConduitMessage++newtype Passphrase = Passphrase {unPassphrase :: BL.ByteString}+ deriving (Eq, Ord, Show)++newtype EncryptedPayload = EncryptedPayload {unEncryptedPayload :: BL.ByteString}+ deriving (Eq, Ord, Show)++newtype ClearPayload = ClearPayload {unClearPayload :: BL.ByteString}+ deriving (Eq, Ord, Show)++newtype WrappedSessionMaterial = WrappedSessionMaterial+ {unWrappedSessionMaterial :: B.ByteString}+ deriving (Eq, Ord, Show)++data SigningAlgorithm = AlgoRSA | AlgoEd25519 | AlgoEd448++type family SecretKeyFor (alg :: SigningAlgorithm) where+ SecretKeyFor 'AlgoRSA = RSATypes.PrivateKey+ SecretKeyFor 'AlgoEd25519 = Ed25519.SecretKey+ SecretKeyFor 'AlgoEd448 = Ed448.SecretKey++type family KeyVersionForSig (v :: Type) :: KeyVersion where+ KeyVersionForSig V4Sig = 'V4+ KeyVersionForSig V6Sig = 'V6++data VersionedPKPayload (v :: KeyVersion) where+ VersionedPKPayloadV4 :: PKPayload 'V4 -> VersionedPKPayload 'V4+ VersionedPKPayloadV6 :: PKPayload 'V6 -> VersionedPKPayload 'V6++asV4PKPayload+ :: SomePKPayload -> Either String (VersionedPKPayload 'V4)+asV4PKPayload (SomePKPayload pk@(PKPayloadV4 _ _ _)) =+ Right (VersionedPKPayloadV4 pk)+asV4PKPayload _ = Left "Expected a v4 PKPayload"++asV6PKPayload+ :: SomePKPayload -> Either String (VersionedPKPayload 'V6)+asV6PKPayload (SomePKPayload pk@(PKPayloadV6 _ _ _)) =+ Right (VersionedPKPayloadV6 pk)+asV6PKPayload _ = Left "Expected a v6 PKPayload"++data Signer (alg :: SigningAlgorithm) (v :: Type) where+ RSASigner+ :: VersionedPKPayload (KeyVersionForSig v)+ -> SecretKeyFor 'AlgoRSA+ -> Signer 'AlgoRSA v+ Ed25519Signer+ :: VersionedPKPayload (KeyVersionForSig v)+ -> SecretKeyFor 'AlgoEd25519+ -> Signer 'AlgoEd25519 v+ Ed448Signer+ :: VersionedPKPayload (KeyVersionForSig v)+ -> SecretKeyFor 'AlgoEd448+ -> Signer 'AlgoEd448 v++class SigningCapability (alg :: SigningAlgorithm) v+instance SigningCapability 'AlgoRSA V4Sig+instance SigningCapability 'AlgoRSA V6Sig+instance SigningCapability 'AlgoEd25519 V4Sig+instance SigningCapability 'AlgoEd25519 V6Sig+instance SigningCapability 'AlgoEd448 V4Sig+instance SigningCapability 'AlgoEd448 V6Sig++mkRSASignerV4+ :: VersionedPKPayload 'V4+ -> SecretKeyFor 'AlgoRSA+ -> Signer 'AlgoRSA V4Sig+mkRSASignerV4 = RSASigner++mkRSASignerV6+ :: VersionedPKPayload 'V6+ -> SecretKeyFor 'AlgoRSA+ -> Signer 'AlgoRSA V6Sig+mkRSASignerV6 = RSASigner++mkEd25519SignerV4+ :: VersionedPKPayload 'V4+ -> SecretKeyFor 'AlgoEd25519+ -> Signer 'AlgoEd25519 V4Sig+mkEd25519SignerV4 = Ed25519Signer++mkEd25519SignerV6+ :: VersionedPKPayload 'V6+ -> SecretKeyFor 'AlgoEd25519+ -> Signer 'AlgoEd25519 V6Sig+mkEd25519SignerV6 = Ed25519Signer++mkEd448SignerV4+ :: VersionedPKPayload 'V4+ -> SecretKeyFor 'AlgoEd448+ -> Signer 'AlgoEd448 V4Sig+mkEd448SignerV4 = Ed448Signer++mkEd448SignerV6+ :: VersionedPKPayload 'V6+ -> SecretKeyFor 'AlgoEd448+ -> Signer 'AlgoEd448 V6Sig+mkEd448SignerV6 = Ed448Signer++data MessageError+ = MessageEncryptError String+ | MessageDecryptError String+ | MessageSignError SignError+ | MessageParseError String+ | MessageParseFailureError MessageParseFailure+ | MessageDecryptFailureError MessageDecryptFailure+ deriving (Eq, Show)++newtype MessageFlow a+ = MessageFlow+ { runMessageFlow :: Either MessageError a+ }++instance Functor MessageFlow where+ fmap f (MessageFlow result) = MessageFlow (fmap f result)++instance Applicative MessageFlow where+ pure = MessageFlow . Right+ MessageFlow ff <*> MessageFlow fa = MessageFlow (ff <*> fa)++instance Monad MessageFlow where+ MessageFlow result >>= f =+ case result of+ Left err -> MessageFlow (Left err)+ Right x -> f x++messageStep :: Either MessageError a -> MessageFlow a+messageStep = MessageFlow++type MessageFlowT m = ExceptT MessageError m++runMessageFlowT :: MessageFlowT m a -> m (Either MessageError a)+runMessageFlowT = runExceptT++liftMessageFlowT :: Monad m => MessageFlow a -> MessageFlowT m a+liftMessageFlowT (MessageFlow result) =+ case result of+ Left err -> throwE err+ Right x -> pure x++data MessageParseFailure+ = MissingEncryptedMessage+ | ExpectedSKESKThenEncryptedData+ | SKESKSEIPDAlgorithmMismatch+ | UnsupportedEncryptedSKESK+ | MissingLiteralDataPacket+ | UnknownCriticalPacketType Word8+ | BrokenCriticalPacketType Word8 String+ deriving (Eq, Show)++data MessageDecryptFailure+ = SessionMaterialDerivationFailed S2KError+ | PayloadDecryptFailed String+ deriving (Eq, Show)++data ParsedEncryptedPayloadKind+ = LegacySEDPayloadKind+ | LegacySEIPDv1PayloadKind+ | SEIPDv2PayloadKind++data EncryptedPreludeKind+ = LegacyEncryptedPreludeKind+ | SEIPDv2EncryptedPreludeKind++data SessionMaterialExposure+ = DoNotExposeSessionMaterial+ | ExposeSessionMaterial+ deriving (Eq, Show)++data EncryptMessageProfile+ = RFC4880Message+ | RFC9580Message++data EncryptMessageOptions (p :: EncryptMessageProfile) where+ RFC4880EncryptMessageOptions+ :: { rfc4880EncryptMessageExposure :: SessionMaterialExposure+ , rfc4880EncryptMessageSymmetricAlgorithm :: SymmetricAlgorithm+ , rfc4880EncryptMessageS2K :: S2K+ , rfc4880EncryptMessageIV :: IV+ }+ -> EncryptMessageOptions 'RFC4880Message+ RFC9580EncryptMessageOptions+ :: { rfc9580EncryptMessageExposure :: SessionMaterialExposure+ , rfc9580EncryptMessageSymmetricAlgorithm :: SymmetricAlgorithm+ , rfc9580EncryptMessageS2K :: S2K+ , rfc9580EncryptMessageIV :: IV+ }+ -> EncryptMessageOptions 'RFC9580Message++deriving instance Eq (EncryptMessageOptions p)+deriving instance Show (EncryptMessageOptions p)++data RecoveredSessionMaterial+ = RecoveredSessionMaterial+ { recoveredSessionAlgorithm :: SymmetricAlgorithm+ , recoveredSessionKey :: SessionKey+ }+ deriving (Eq, Show)++renderMessageParseFailure :: MessageParseFailure -> String+renderMessageParseFailure MissingEncryptedMessage =+ "Could not parse encrypted OpenPGP message"+renderMessageParseFailure ExpectedSKESKThenEncryptedData =+ "Expected an SKESK packet followed by symmetrically encrypted data or SEIPD v2 data"+renderMessageParseFailure SKESKSEIPDAlgorithmMismatch =+ "SKESK and SEIPD v2 algorithms do not match"+renderMessageParseFailure UnsupportedEncryptedSKESK =+ "Cannot decrypt SKESK packets with encrypted session keys"+renderMessageParseFailure MissingLiteralDataPacket =+ "Decrypted message does not contain a literal data packet"+renderMessageParseFailure (UnknownCriticalPacketType t) =+ "Unknown critical packet type: " ++ show t+renderMessageParseFailure (BrokenCriticalPacketType t err) =+ "Broken critical packet type " ++ show t ++ ": " ++ err++renderMessageDecryptFailure :: MessageDecryptFailure -> String+renderMessageDecryptFailure (SessionMaterialDerivationFailed err) = renderS2KError err+renderMessageDecryptFailure (PayloadDecryptFailed err) = err++mkPassphrase :: BL.ByteString -> Passphrase+mkPassphrase = Passphrase++passphraseBytes :: Passphrase -> BL.ByteString+passphraseBytes = unPassphrase++mkEncryptedPayload :: BL.ByteString -> EncryptedPayload+mkEncryptedPayload = EncryptedPayload++mkClearPayload :: BL.ByteString -> ClearPayload+mkClearPayload = ClearPayload++clearPayloadBytes :: ClearPayload -> BL.ByteString+clearPayloadBytes = unClearPayload++encryptedPayloadBytes :: EncryptedPayload -> BL.ByteString+encryptedPayloadBytes = unEncryptedPayload++firstLeft :: (e -> e') -> Either e a -> Either e' a+firstLeft f = either (Left . f) Right++-- | Lift a parse failure step into the unified MessageError channel+parseStep :: Either MessageParseFailure a -> MessageFlow a+parseStep = messageStep . firstLeft MessageParseFailureError++-- | Lift a decrypt failure step into the unified MessageError channel+decryptStep :: Either MessageDecryptFailure a -> MessageFlow a+decryptStep = messageStep . firstLeft MessageDecryptFailureError++-- | Lift a string encrypt error step into the unified MessageError channel+encryptStep :: Either String a -> MessageFlow a+encryptStep = messageStep . firstLeft MessageEncryptError++-- | Lift a sign error step into the unified MessageError channel+signStep :: Either SignError a -> MessageFlow a+signStep = messageStep . firstLeft MessageSignError++signStepT :: Monad m => Either SignError a -> MessageFlowT m a+signStepT = liftMessageFlowT . signStep++signBackendStep :: Either String a -> Either SignError a+signBackendStep = firstLeft SignBackendError++decryptSessionStep+ :: Either S2KError a -> Either MessageDecryptFailure a+decryptSessionStep = firstLeft SessionMaterialDerivationFailed++decryptSessionKeySizeStep+ :: Either CipherError a -> Either MessageDecryptFailure a+decryptSessionKeySizeStep =+ firstLeft+ (SessionMaterialDerivationFailed . S2KUnsupportedAlgorithm)++decryptCipherStep+ :: Either CipherError a -> Either MessageDecryptFailure a+decryptCipherStep = firstLeft (PayloadDecryptFailed . renderCipherError)++decryptPayloadStep+ :: Either String a -> Either MessageDecryptFailure a+decryptPayloadStep = firstLeft PayloadDecryptFailed++encryptMessage+ :: EncryptMessageOptions p+ -> Passphrase+ -> ClearPayload+ -> Either+ MessageError+ (EncryptedPayload, Maybe RecoveredSessionMaterial)+encryptMessage options passphrase payload = runMessageFlow $+ case options of+ RFC4880EncryptMessageOptions exposure sa s2k iv -> do+ encryptedPayload <-+ encryptMessageWithRFC4880Fallback sa s2k iv passphrase payload+ sessionKeyMaterial <- deriveSessionMaterial sa s2k passphrase+ pure+ ( encryptedPayload+ , exposedSessionMaterial exposure sa sessionKeyMaterial+ )+ RFC9580EncryptMessageOptions exposure sa s2k iv -> do+ encryptStep $ validateRFC9580MessageSymmetric defaultPolicy sa+ encryptStep $ validateModernMessageS2K defaultPolicy s2k+ encrypted <-+ encryptStep $+ encryptSEIPDv2WithSKESKBlock+ sa+ (messageDefaultAEADAlgorithm messagePolicy)+ (messageDefaultChunkSize messagePolicy)+ ( defaultSEIPDv2SaltFromIV+ (messageSEIPDv2SaltOctets messagePolicy)+ iv+ )+ s2k+ (unPassphrase passphrase)+ ( Block+ [LiteralDataPkt BinaryData BL.empty 0 (unClearPayload payload)]+ )+ sessionKeyMaterial <- deriveSessionMaterial sa s2k passphrase+ pure+ ( EncryptedPayload (runPut (put (Block encrypted)))+ , exposedSessionMaterial exposure sa sessionKeyMaterial+ )+ where+ messagePolicy = policyMessageEncryption defaultPolicy++defaultSEIPDv2SaltFromIV :: Int -> IV -> Salt+defaultSEIPDv2SaltFromIV outputLen (IV ivBytes) =+ Salt (B.take outputLen (B.concat (replicate outputLen seed)))+ where+ seed+ | B.null ivBytes = B.singleton 0+ | otherwise = ivBytes++encryptMessageWithRFC4880Fallback+ :: SymmetricAlgorithm+ -> S2K+ -> IV+ -> Passphrase+ -> ClearPayload+ -> MessageFlow EncryptedPayload+encryptMessageWithRFC4880Fallback sa s2k iv passphrase payload = do+ keyLen <-+ encryptStep . first renderCipherError $ keySize sa+ sessionMaterial <-+ encryptStep . first renderS2KError $+ WrappedSessionMaterial+ <$> string2Key s2k keyLen (unPassphrase passphrase)+ let literal =+ LiteralDataPkt BinaryData BL.empty 0 (unClearPayload payload)+ cleartext = BL.toStrict (runPut (put (Block [literal])))+ cleartextWithMDC = cleartext <> mdcTrailerForSEIPDv1 iv cleartext+ encrypted <-+ encryptStep . first renderCipherError $+ encryptOpenPGPCfbRaw+ OpenPGPCFBNoResyncW+ sa+ iv+ cleartextWithMDC+ (unWrappedSessionMaterial sessionMaterial)+ return . EncryptedPayload . runPut . put $+ Block+ [ SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k Nothing))+ , SymEncIntegrityProtectedDataPkt+ (SEIPD1 1 (BL.fromStrict encrypted))+ ]++deriveSessionMaterial+ :: SymmetricAlgorithm+ -> S2K+ -> Passphrase+ -> MessageFlow B.ByteString+deriveSessionMaterial sa s2k passphrase = do+ keyLen <- encryptStep . first renderCipherError $ keySize sa+ encryptStep . first renderS2KError $+ string2Key s2k keyLen (unPassphrase passphrase)++exposedSessionMaterial+ :: SessionMaterialExposure+ -> SymmetricAlgorithm+ -> B.ByteString+ -> Maybe RecoveredSessionMaterial+exposedSessionMaterial DoNotExposeSessionMaterial _ _ = Nothing+exposedSessionMaterial ExposeSessionMaterial sa sessionKeyMaterial =+ Just+ ( RecoveredSessionMaterial+ { recoveredSessionAlgorithm = sa+ , recoveredSessionKey = SessionKey sessionKeyMaterial+ }+ )++decryptMessage+ :: Passphrase+ -> EncryptedPayload+ -> Either MessageError ClearPayload+decryptMessage passphrase encrypted = runMessageFlow $ do+ encryptedPackets <-+ parseStep $+ rejectUnknownCriticalPacketsTyped+ (parsePkts (unEncryptedPayload encrypted))+ payload <-+ parseStep $ extractEncryptedPayload encryptedPackets+ cleartext <-+ decryptStep $ decryptPayloadTyped passphrase payload+ clearPackets <-+ parseStep $+ rejectUnknownCriticalPacketsTyped+ (parsePkts (unClearPayload cleartext))+ parseStep $ extractLiteralPayload clearPackets++signMessageWith+ :: (MonadRandom m, SigningCapability alg v)+ => Signer alg v+ -> ClearPayload+ -> m (Either MessageError BL.ByteString)+signMessageWith signer payload = runMessageFlowT $+ case signer of+ RSASigner signerPK signingKey ->+ case signerPK of+ VersionedPKPayloadV4 pk ->+ signV4Message+ pk+ ( \hashed unhashed clear ->+ let builder =+ sigBuilderInitTyped @'PKA.RSA RFC9580W BinarySig SHA512W+ withHashed = addHashedSubs (listToHashedSubs hashed) builder+ withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed+ in signDataWithRSABuilder withUnhashed signingKey clear+ )+ payload+ VersionedPKPayloadV6 pk ->+ signV6Message+ pk+ ( \salt hashed unhashed clear ->+ let builder =+ sigBuilderInitV6Typed @'PKA.RSA RFC9580W BinarySig SHA512W salt+ withHashed = addHashedSubs (listToHashedSubs hashed) builder+ withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed+ in signDataWithRSAV6Builder withUnhashed signingKey clear+ )+ payload+ Ed25519Signer signerPK signingKey ->+ case signerPK of+ VersionedPKPayloadV4 pk ->+ signV4Message+ pk+ ( \hashed unhashed clear ->+ let builder =+ sigBuilderInitTyped @'PKA.Ed25519 RFC9580W BinarySig SHA512W+ withHashed = addHashedSubs (listToHashedSubs hashed) builder+ withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed+ in signDataWithEd25519Builder withUnhashed signingKey clear+ )+ payload+ VersionedPKPayloadV6 pk ->+ signV6Message+ pk+ ( \salt hashed unhashed clear ->+ let builder =+ sigBuilderInitV6Typed @'PKA.Ed25519+ RFC9580W+ BinarySig+ SHA512W+ salt+ withHashed = addHashedSubs (listToHashedSubs hashed) builder+ withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed+ in signDataWithEd25519V6Builder withUnhashed signingKey clear+ )+ payload+ Ed448Signer signerPK signingKey ->+ case signerPK of+ VersionedPKPayloadV4 pk ->+ signV4Message+ pk+ ( \hashed unhashed clear ->+ let builder =+ sigBuilderInitTyped @'PKA.Ed448 RFC9580W BinarySig SHA512W+ withHashed = addHashedSubs (listToHashedSubs hashed) builder+ withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed+ in signDataWithEd448Builder withUnhashed signingKey clear+ )+ payload+ VersionedPKPayloadV6 pk ->+ signV6Message+ pk+ ( \salt hashed unhashed clear ->+ let builder =+ sigBuilderInitV6Typed @'PKA.Ed448 RFC9580W BinarySig SHA512W salt+ withHashed = addHashedSubs (listToHashedSubs hashed) builder+ withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed+ in signDataWithEd448V6Builder withUnhashed signingKey clear+ )+ payload++signMessage+ :: (MonadRandom m, SigningCapability alg v)+ => Signer alg v+ -> BL.ByteString+ -> m (Either MessageError BL.ByteString)+signMessage signer = signMessageWith signer . mkClearPayload++versionedPKPayload :: VersionedPKPayload v -> PKPayload v+versionedPKPayload (VersionedPKPayloadV4 pk) = pk+versionedPKPayload (VersionedPKPayloadV6 pk) = pk++verifySignedMessage+ :: ConduitMessage.VerificationOptions+ -> PublicKeyring+ -> BL.ByteString+ -> [Either VerificationError Verification]+verifySignedMessage = ConduitMessage.verifyMessage++signV4Message+ :: Monad m+ => PKPayload 'V4+ -> ( [SigSubPacket]+ -> [SigSubPacket]+ -> BL.ByteString+ -> Either SignError SignaturePayload+ )+ -> ClearPayload+ -> MessageFlowT m BL.ByteString+signV4Message signer signingFn payload =+ signStepT (signV4WithIssuers signer signingFn payload)++signV6Message+ :: MonadRandom m+ => PKPayload 'V6+ -> ( SignatureSalt+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> BL.ByteString+ -> Either SignError SignaturePayload+ )+ -> ClearPayload+ -> MessageFlowT m BL.ByteString+signV6Message signer signingFn payload = do+ salt <- lift randomSHA512SignatureSalt+ signStepT+ (signV6WithFingerprintOnly signer (signingFn salt) payload)++randomSHA512SignatureSalt :: MonadRandom m => m SignatureSalt+randomSHA512SignatureSalt =+ SignatureSalt . BL.fromStrict <$> getRandomBytes 32++signV4WithIssuers+ :: PKPayload 'V4+ -> ( [SigSubPacket]+ -> [SigSubPacket]+ -> BL.ByteString+ -> Either SignError SignaturePayload+ )+ -> ClearPayload+ -> Either SignError BL.ByteString+signV4WithIssuers signer signingFn payload = do+ issuerKeyId <-+ signBackendStep (eightOctetKeyID (SomePKPayload signer))+ let hashed =+ [ SigSubPacket+ False+ ( IssuerFingerprint+ IssuerFingerprintV4+ (fingerprint (SomePKPayload signer))+ )+ ]+ unhashed = [SigSubPacket False (Issuer issuerKeyId)]+ signWithSubpackets hashed unhashed signingFn payload++signV6WithFingerprintOnly+ :: PKPayload 'V6+ -> ( [SigSubPacket]+ -> [SigSubPacket]+ -> BL.ByteString+ -> Either SignError SignaturePayload+ )+ -> ClearPayload+ -> Either SignError BL.ByteString+signV6WithFingerprintOnly signer signingFn payload = do+ let hashed =+ [ SigSubPacket+ False+ ( IssuerFingerprint+ IssuerFingerprintV6+ (fingerprint (SomePKPayload signer))+ )+ ]+ unhashed = []+ signWithSubpackets hashed unhashed signingFn payload++signWithSubpackets+ :: [SigSubPacket]+ -> [SigSubPacket]+ -> ( [SigSubPacket]+ -> [SigSubPacket]+ -> BL.ByteString+ -> Either SignError SignaturePayload+ )+ -> ClearPayload+ -> Either SignError BL.ByteString+signWithSubpackets hashed unhashed signingFn payload = do+ let clear = unClearPayload payload+ literal = LiteralDataPkt BinaryData BL.empty 0 clear+ signature <- signingFn hashed unhashed clear+ return . runPut . put $ Block [literal, SignaturePkt signature]++encryptOpenPGPCfb+ :: SymmetricAlgorithm+ -> IV+ -> B.ByteString+ -> WrappedSessionMaterial+ -> Either CipherError B.ByteString+encryptOpenPGPCfb sa iv cleartext (WrappedSessionMaterial keydata) =+ encryptOpenPGPCfbRaw OpenPGPCFBResyncW sa iv cleartext keydata++extractEncryptedPayload+ :: [Pkt] -> Either MessageParseFailure SomeParsedEncryptedPayload+extractEncryptedPayload =+ fmap parsedEncryptedPayloadFromPrelude+ . extractEncryptedPreludeTyped++data EncryptedPrelude (k :: EncryptedPreludeKind) where+ LegacySEDPrelude+ :: SKESK 'SKESKV4+ -> B.ByteString+ -> EncryptedPrelude 'LegacyEncryptedPreludeKind+ LegacySEIPDv1Prelude+ :: SKESK 'SKESKV4+ -> B.ByteString+ -> EncryptedPrelude 'LegacyEncryptedPreludeKind+ SEIPDv2SKESK4Prelude+ :: SymmetricAlgorithm+ -> S2K+ -> AEADAlgorithm+ -> Word8+ -> Salt+ -> B.ByteString+ -> EncryptedPrelude 'SEIPDv2EncryptedPreludeKind+ SEIPDv2SKESK6Prelude+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> S2K+ -> BL.ByteString+ -> BL.ByteString+ -> BL.ByteString+ -> Word8+ -> Salt+ -> B.ByteString+ -> EncryptedPrelude 'SEIPDv2EncryptedPreludeKind++data SomeEncryptedPrelude where+ SomeEncryptedPrelude+ :: EncryptedPrelude k -> SomeEncryptedPrelude++extractEncryptedPreludeTyped+ :: [Pkt] -> Either MessageParseFailure SomeEncryptedPrelude+extractEncryptedPreludeTyped+ ( SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k esk))+ : SymEncDataPkt payload+ : _+ ) =+ Right+ ( SomeEncryptedPrelude+ ( LegacySEDPrelude+ (SKESK4Packet sa s2k esk)+ (BL.toStrict payload)+ )+ )+extractEncryptedPreludeTyped+ ( SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k esk))+ : SymEncIntegrityProtectedDataPkt (SEIPD1 _ payload)+ : _+ ) =+ Right+ ( SomeEncryptedPrelude+ ( LegacySEIPDv1Prelude+ (SKESK4Packet sa s2k esk)+ (BL.toStrict payload)+ )+ )+extractEncryptedPreludeTyped+ ( SKESKPkt skesk+ : SymEncIntegrityProtectedDataPkt+ (SEIPD2 payloadSA aead chunkSize salt payload)+ : _+ ) =+ toSEIPDv2Prelude skesk payloadSA aead chunkSize salt payload+extractEncryptedPreludeTyped [] = Left MissingEncryptedMessage+extractEncryptedPreludeTyped _ = Left ExpectedSKESKThenEncryptedData++toSEIPDv2Prelude+ :: SKESKPayload+ -> SymmetricAlgorithm+ -> AEADAlgorithm+ -> Word8+ -> Salt+ -> BL.ByteString+ -> Either MessageParseFailure SomeEncryptedPrelude+toSEIPDv2Prelude (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k Nothing)) payloadSA aead chunkSize salt payload+ | sa /= payloadSA = Left SKESKSEIPDAlgorithmMismatch+ | otherwise =+ Right+ ( SomeEncryptedPrelude+ ( SEIPDv2SKESK4Prelude+ sa+ s2k+ aead+ chunkSize+ salt+ (BL.toStrict payload)+ )+ )+toSEIPDv2Prelude (SKESKPayloadV6Packet (SKESKPayloadV6 sa aa s2k iv esk tag)) payloadSA aead chunkSize salt payload+ | sa /= payloadSA = Left SKESKSEIPDAlgorithmMismatch+ | otherwise =+ Right+ ( SomeEncryptedPrelude+ ( SEIPDv2SKESK6Prelude+ sa+ aa+ s2k+ iv+ esk+ tag+ chunkSize+ salt+ (BL.toStrict payload)+ )+ )+toSEIPDv2Prelude (SKESKPayloadV4Packet (SKESKPayloadV4 _ _ (Just _))) _ _ _ _ _ = Left UnsupportedEncryptedSKESK++parsedEncryptedPayloadFromPrelude+ :: SomeEncryptedPrelude -> SomeParsedEncryptedPayload+parsedEncryptedPayloadFromPrelude (SomeEncryptedPrelude prelude) =+ case prelude of+ LegacySEDPrelude skesk payload ->+ SomeParsedEncryptedPayload (LegacySEDPayload skesk payload)+ LegacySEIPDv1Prelude skesk payload ->+ SomeParsedEncryptedPayload (LegacySEIPDv1Payload skesk payload)+ SEIPDv2SKESK4Prelude sa s2k aead chunkSize salt payload ->+ SomeParsedEncryptedPayload+ ( SEIPDv2Payload+ sa+ aead+ chunkSize+ salt+ (SEIPDv2SKESK4 sa s2k)+ payload+ )+ SEIPDv2SKESK6Prelude sa aa s2k iv esk tag chunkSize salt payload ->+ SomeParsedEncryptedPayload+ ( SEIPDv2Payload+ sa+ aa+ chunkSize+ salt+ (SEIPDv2SKESK6 sa aa s2k iv esk tag)+ payload+ )++data SEIPDv2SKESKInfo (v :: KeyVersion) where+ SEIPDv2SKESK4+ :: SymmetricAlgorithm -> S2K -> SEIPDv2SKESKInfo 'V4+ SEIPDv2SKESK6+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> S2K+ -> BL.ByteString+ -> BL.ByteString+ -> BL.ByteString+ -> SEIPDv2SKESKInfo 'V6++data ParsedEncryptedPayload (k :: ParsedEncryptedPayloadKind) where+ LegacySEDPayload+ :: SKESK 'SKESKV4+ -> B.ByteString+ -> ParsedEncryptedPayload 'LegacySEDPayloadKind+ LegacySEIPDv1Payload+ :: SKESK 'SKESKV4+ -> B.ByteString+ -> ParsedEncryptedPayload 'LegacySEIPDv1PayloadKind+ SEIPDv2Payload+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> Word8+ -> Salt+ -> SEIPDv2SKESKInfo v+ -> B.ByteString+ -> ParsedEncryptedPayload 'SEIPDv2PayloadKind++data SomeParsedEncryptedPayload where+ SomeParsedEncryptedPayload+ :: ParsedEncryptedPayload k+ -> SomeParsedEncryptedPayload++decryptPayload+ :: Passphrase+ -> SomeParsedEncryptedPayload+ -> Either String ClearPayload+decryptPayload passphrase =+ first renderMessageDecryptFailure+ . decryptPayloadTyped passphrase++decryptPayloadTyped+ :: Passphrase+ -> SomeParsedEncryptedPayload+ -> Either MessageDecryptFailure ClearPayload+decryptPayloadTyped passphrase (SomeParsedEncryptedPayload payload) =+ case payload of+ LegacySEDPayload skesk encryptedPayload ->+ decryptLegacySEDPayloadTyped passphrase skesk encryptedPayload+ LegacySEIPDv1Payload skesk encryptedPayload ->+ decryptLegacySEIPDv1PayloadTyped+ passphrase+ skesk+ encryptedPayload+ SEIPDv2Payload sa aead chunkSize salt skeskInfo encryptedPayload ->+ decryptSEIPDv2PayloadTyped+ passphrase+ sa+ aead+ chunkSize+ salt+ skeskInfo+ encryptedPayload++decryptLegacySEDPayloadTyped+ :: Passphrase+ -> SKESK 'SKESKV4+ -> B.ByteString+ -> Either MessageDecryptFailure ClearPayload+decryptLegacySEDPayloadTyped passphrase skesk payload = do+ (sessionAlgorithm, sessionKeyBytes) <-+ decryptSessionStep $+ skesk2SessionKey skesk (unPassphrase passphrase)+ decryptCipherStep $+ ClearPayload . BL.fromStrict+ <$> decryptOpenPGPCfb+ sessionAlgorithm+ payload+ sessionKeyBytes++decryptLegacySEIPDv1PayloadTyped+ :: Passphrase+ -> SKESK 'SKESKV4+ -> B.ByteString+ -> Either MessageDecryptFailure ClearPayload+decryptLegacySEIPDv1PayloadTyped passphrase skesk payload = do+ (sessionAlgorithm, sessionKeyBytes) <-+ decryptSessionStep $+ skesk2SessionKey skesk (unPassphrase passphrase)+ (nonce, decrypted) <-+ decryptCipherStep $+ decryptPreservingNonce sessionAlgorithm payload sessionKeyBytes+ cleartext <-+ decryptPayloadStep $ validateSEIPD1MDC nonce decrypted+ Right (ClearPayload (BL.fromStrict cleartext))++decryptSEIPDv2PayloadTyped+ :: Passphrase+ -> SymmetricAlgorithm+ -> AEADAlgorithm+ -> Word8+ -> Salt+ -> SEIPDv2SKESKInfo v+ -> B.ByteString+ -> Either MessageDecryptFailure ClearPayload+decryptSEIPDv2PayloadTyped passphrase sa aead chunkSize salt skeskInfo payload = do+ sessionKey <-+ SessionKey <$> deriveSEIPDv2SessionKeyBytes passphrase skeskInfo+ decryptPayloadStep $+ ClearPayload . BL.fromStrict+ <$> decryptSEIPDv2Payload sa aead chunkSize salt payload sessionKey++deriveSEIPDv2SessionKeyBytes+ :: Passphrase+ -> SEIPDv2SKESKInfo v+ -> Either MessageDecryptFailure B.ByteString+deriveSEIPDv2SessionKeyBytes passphrase (SEIPDv2SKESK4 sa s2k) =+ deriveSessionKeyBytes passphrase sa s2k+deriveSEIPDv2SessionKeyBytes passphrase (SEIPDv2SKESK6 sa aead s2k iv esk tag) = do+ ikm <- deriveSessionKeyBytes passphrase sa s2k+ kek <- decryptPayloadStep $ deriveSKESK6KEK sa aead ikm+ decryptPayloadStep $+ decryptSKESK6SessionKey+ sa+ aead+ kek+ (BL.toStrict iv)+ (BL.toStrict esk)+ (BL.toStrict tag)++deriveSessionKeyBytes+ :: Passphrase+ -> SymmetricAlgorithm+ -> S2K+ -> Either MessageDecryptFailure B.ByteString+deriveSessionKeyBytes passphrase sa s2k = do+ keyLen <- decryptSessionKeySizeStep $ keySize sa+ decryptSessionStep $+ string2Key s2k keyLen (unPassphrase passphrase)++extractLiteralPayload+ :: [Pkt] -> Either MessageParseFailure ClearPayload+extractLiteralPayload pkts =+ case [p | LiteralDataPkt _ _ _ p <- pkts] of+ payload : _ -> Right (ClearPayload payload)+ [] -> Left MissingLiteralDataPacket++rejectUnknownCriticalPacketsTyped+ :: [Pkt] -> Either MessageParseFailure [Pkt]+rejectUnknownCriticalPacketsTyped =+ go []+ where+ go acc [] = Right (reverse acc)+ go acc (pkt : rest) =+ case pkt of+ OtherPacketPkt t _ | t < 40 -> Left (UnknownCriticalPacketType t)+ BrokenPacketPkt err t _ | t < 40 -> Left (BrokenCriticalPacketType t err)+ _ -> go (pkt : acc) rest++validateModernMessageS2K+ :: OpenPGPPolicy -> S2K -> Either String ()+validateModernMessageS2K policy s2k =+ case s2kHashAlgorithm s2k of+ Just ha+ | ha+ `elem` deprecatedHashAlgorithms (policyGenerationDeprecations policy) ->+ Left+ ( "deprecated hash algorithm disallowed for modern message generation: "+ ++ show ha+ )+ _ -> Right ()++validateRFC9580MessageSymmetric+ :: OpenPGPPolicy -> SymmetricAlgorithm -> Either String ()+validateRFC9580MessageSymmetric policy sa+ | supportsSEIPDv2Symmetric policy sa = Right ()+ | otherwise =+ Left+ ( "symmetric algorithm disallowed for RFC9580 message generation: "+ ++ show sa+ ) s2kHashAlgorithm :: S2K -> Maybe HashAlgorithm s2kHashAlgorithm (Simple ha) = Just ha
Codec/Encryption/OpenPGP/Policy.hs view
@@ -2,7 +2,6 @@ -- Copyright © 2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).- {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-}@@ -10,74 +9,87 @@ {-# LANGUAGE TypeFamilies #-} module Codec.Encryption.OpenPGP.Policy- ( OpenPGPRFC(..)- , OpenPGPRFCW(..)- , SomeOpenPGPRFCW(..)- , promoteOpenPGPRFC- , demoteOpenPGPRFCW- , HashAlgorithmW(..)- , SomeHashAlgorithmW(..)- , promoteHashAlgorithm- , demoteHashAlgorithmW- , HashAlgoStatus(..)- , HashAlgoStatusFor- , HashAlgoAllowedFor- , HashAlgoVerifiableFor- , PKESKVersionPolicy(..)- , MessageEncryptionPolicy(..)- , SecretKeyProtectionPolicy(..)- , GenerationDeprecationPolicy(..)- , VerificationDefaults(..)- , DecryptPolicy(..)- , OpenPGPPolicy(..)- , policyForRFC- , defaultPolicy- , defaultPKESKVersionPolicy- , defaultVerificationDefaults- , defaultDecryptPolicy- , lenientDecryptPolicy- , supportsSEIPDv2Symmetric- , secretKeyProtectionPolicyForKeyVersion- , signatureV6SaltSizeForHashAlgorithm- , ecdhKdfHashDigest- , validateTable30PolicyForRecipient- , legacySecretKeyProtectionErrorMessage- -- * Signature context validation (RFC9580/RFC4880)- , isAllowedPrimaryKeySig- , isAllowedSubkeySig- , isAllowedUIDSig- -- * Signature type validation helpers (for parser use)- , isAllowedPrimaryKeySigType- , isAllowedSubkeySigType- , isAllowedUIDSigType- ) where+ ( OpenPGPRFC (..)+ , OpenPGPRFCW (..)+ , SomeOpenPGPRFCW (..)+ , promoteOpenPGPRFC+ , demoteOpenPGPRFCW+ , HashAlgorithmW (..)+ , SomeHashAlgorithmW (..)+ , promoteHashAlgorithm+ , demoteHashAlgorithmW+ , HashAlgoStatus (..)+ , HashAlgoStatusFor+ , HashAlgoAllowedFor+ , HashAlgoVerifiableFor+ , PKESKVersionPolicy (..)+ , MessageEncryptionPolicy (..)+ , SecretKeyProtectionPolicy (..)+ , GenerationDeprecationPolicy (..)+ , VerificationDefaults (..)+ , DecryptPolicy (..)+ , OpenPGPPolicy (..)+ , policyForRFC+ , defaultPolicy+ , defaultPKESKVersionPolicy+ , defaultVerificationDefaults+ , defaultDecryptPolicy+ , lenientDecryptPolicy+ , supportsSEIPDv2Symmetric+ , secretKeyProtectionPolicyForKeyVersion+ , signatureV6SaltSizeForHashAlgorithm+ , ecdhKdfHashDigest+ , validateTable30PolicyForRecipient+ , legacySecretKeyProtectionErrorMessage -import Codec.Encryption.OpenPGP.Types-import Codec.Encryption.OpenPGP.SignatureQualities (sigType)+ -- * Signature verification policy+ , VerificationPolicy (..)+ , VerificationPolicyAction (..)+ , defaultVerificationPolicy+ , strictVerificationPolicy+ , lenientVerificationPolicy+ , isVerificationError+ , isVerificationWarning+ , applyVerificationPolicy++ -- * Signature context validation (RFC9580/RFC4880)+ , isAllowedPrimaryKeySig+ , isAllowedSubkeySig+ , isAllowedUIDSig++ -- * Signature type validation helpers (for parser use)+ , isAllowedPrimaryKeySigType+ , isAllowedSubkeySigType+ , isAllowedUIDSigType+ ) where+ import qualified Crypto.Hash as CH import qualified Crypto.Hash.Algorithms as CHAlg import qualified Crypto.PubKey.ECC.ECDSA as ECDSA import qualified Crypto.PubKey.ECC.Types as ECCT import qualified Data.ByteArray as BA import qualified Data.ByteString as B-import Data.List (elem) import Data.Kind (Constraint)+import Data.List (elem) import Data.Word (Word8)-import GHC.TypeLits (ErrorMessage(..), TypeError)+import GHC.TypeLits (ErrorMessage (..), TypeError) +import Codec.Encryption.OpenPGP.SignatureQualities (sigType)+import Codec.Encryption.OpenPGP.Types+ data OpenPGPRFC- = RFC2440- | RFC4880- | RFC9580- deriving (Eq, Show)+ = RFC2440+ | RFC4880+ | RFC9580+ deriving (Eq, Show) data OpenPGPRFCW (rfc :: OpenPGPRFC) where- RFC2440W :: OpenPGPRFCW 'RFC2440- RFC4880W :: OpenPGPRFCW 'RFC4880- RFC9580W :: OpenPGPRFCW 'RFC9580+ RFC2440W :: OpenPGPRFCW 'RFC2440+ RFC4880W :: OpenPGPRFCW 'RFC4880+ RFC9580W :: OpenPGPRFCW 'RFC9580 data SomeOpenPGPRFCW where- SomeOpenPGPRFCW :: OpenPGPRFCW rfc -> SomeOpenPGPRFCW+ SomeOpenPGPRFCW :: OpenPGPRFCW rfc -> SomeOpenPGPRFCW demoteOpenPGPRFCW :: OpenPGPRFCW rfc -> OpenPGPRFC demoteOpenPGPRFCW RFC2440W = RFC2440@@ -90,18 +102,18 @@ promoteOpenPGPRFC RFC9580 = SomeOpenPGPRFCW RFC9580W data HashAlgorithmW (h :: HashAlgorithm) where- DeprecatedMD5W :: HashAlgorithmW 'DeprecatedMD5- SHA1W :: HashAlgorithmW 'SHA1- RIPEMD160W :: HashAlgorithmW 'RIPEMD160- SHA256W :: HashAlgorithmW 'SHA256- SHA384W :: HashAlgorithmW 'SHA384- SHA512W :: HashAlgorithmW 'SHA512- SHA224W :: HashAlgorithmW 'SHA224- SHA3_256W :: HashAlgorithmW 'SHA3_256- SHA3_512W :: HashAlgorithmW 'SHA3_512+ DeprecatedMD5W :: HashAlgorithmW 'DeprecatedMD5+ SHA1W :: HashAlgorithmW 'SHA1+ RIPEMD160W :: HashAlgorithmW 'RIPEMD160+ SHA256W :: HashAlgorithmW 'SHA256+ SHA384W :: HashAlgorithmW 'SHA384+ SHA512W :: HashAlgorithmW 'SHA512+ SHA224W :: HashAlgorithmW 'SHA224+ SHA3_256W :: HashAlgorithmW 'SHA3_256+ SHA3_512W :: HashAlgorithmW 'SHA3_512 data SomeHashAlgorithmW where- SomeHashAlgorithmW :: HashAlgorithmW h -> SomeHashAlgorithmW+ SomeHashAlgorithmW :: HashAlgorithmW h -> SomeHashAlgorithmW demoteHashAlgorithmW :: HashAlgorithmW h -> HashAlgorithm demoteHashAlgorithmW DeprecatedMD5W = DeprecatedMD5@@ -126,7 +138,8 @@ promoteHashAlgorithm SHA3_512 = Just (SomeHashAlgorithmW SHA3_512W) promoteHashAlgorithm (OtherHA _) = Nothing -signatureV6SaltSizeForHashAlgorithm :: HashAlgorithm -> Maybe Word8+signatureV6SaltSizeForHashAlgorithm+ :: HashAlgorithm -> Maybe Word8 signatureV6SaltSizeForHashAlgorithm SHA224 = Just 16 signatureV6SaltSizeForHashAlgorithm SHA256 = Just 16 signatureV6SaltSizeForHashAlgorithm SHA384 = Just 24@@ -136,200 +149,330 @@ signatureV6SaltSizeForHashAlgorithm _ = Nothing data HashAlgoStatus- = HashAllowed- | HashShouldNot- | HashMustNot+ = HashAllowed+ | HashShouldNot+ | HashMustNot -type family HashAlgoStatusFor (rfc :: OpenPGPRFC) (h :: HashAlgorithm) :: HashAlgoStatus where- HashAlgoStatusFor 'RFC4880 'DeprecatedMD5 = 'HashShouldNot- HashAlgoStatusFor 'RFC9580 'DeprecatedMD5 = 'HashMustNot- HashAlgoStatusFor 'RFC9580 'SHA1 = 'HashMustNot- HashAlgoStatusFor 'RFC9580 'RIPEMD160 = 'HashShouldNot- HashAlgoStatusFor rfc h = 'HashAllowed+type family+ HashAlgoStatusFor (rfc :: OpenPGPRFC) (h :: HashAlgorithm)+ :: HashAlgoStatus+ where+ HashAlgoStatusFor 'RFC4880 'DeprecatedMD5 = 'HashShouldNot+ HashAlgoStatusFor 'RFC9580 'DeprecatedMD5 = 'HashMustNot+ HashAlgoStatusFor 'RFC9580 'SHA1 = 'HashMustNot+ HashAlgoStatusFor 'RFC9580 'RIPEMD160 = 'HashShouldNot+ HashAlgoStatusFor rfc h = 'HashAllowed type family AssertHashAllowed (status :: HashAlgoStatus) :: Constraint where- AssertHashAllowed 'HashAllowed = ()- AssertHashAllowed 'HashShouldNot =- TypeError ('Text "Hash algorithm is deprecated (SHOULD NOT) for new signatures under this RFC")- AssertHashAllowed 'HashMustNot =- TypeError ('Text "Hash algorithm is disallowed (MUST NOT) for new signatures under this RFC")+ AssertHashAllowed 'HashAllowed = ()+ AssertHashAllowed 'HashShouldNot =+ TypeError+ ( 'Text+ "Hash algorithm is deprecated (SHOULD NOT) for new signatures under this RFC"+ )+ AssertHashAllowed 'HashMustNot =+ TypeError+ ( 'Text+ "Hash algorithm is disallowed (MUST NOT) for new signatures under this RFC"+ ) -type HashAlgoAllowedFor rfc h = AssertHashAllowed (HashAlgoStatusFor rfc h)+type HashAlgoAllowedFor rfc h =+ AssertHashAllowed (HashAlgoStatusFor rfc h) type family AssertHashVerifiable (status :: HashAlgoStatus) :: Constraint where- AssertHashVerifiable 'HashAllowed = ()- AssertHashVerifiable 'HashShouldNot = ()- AssertHashVerifiable 'HashMustNot =- TypeError ('Text "Hash algorithm is disallowed for verification in this RFC context")+ AssertHashVerifiable 'HashAllowed = ()+ AssertHashVerifiable 'HashShouldNot = ()+ AssertHashVerifiable 'HashMustNot =+ TypeError+ ( 'Text+ "Hash algorithm is disallowed for verification in this RFC context"+ ) -type HashAlgoVerifiableFor rfc h = AssertHashVerifiable (HashAlgoStatusFor rfc h)+type HashAlgoVerifiableFor rfc h =+ AssertHashVerifiable (HashAlgoStatusFor rfc h) data PKESKVersionPolicy- = PreferV6- | ForceV3Interop- deriving (Eq, Show)+ = PreferV6+ | ForceV3Interop+ deriving (Eq, Show) data MessageEncryptionPolicy = MessageEncryptionPolicy- { messageDefaultSymmetricAlgorithm :: SymmetricAlgorithm- , messageDefaultAEADAlgorithm :: AEADAlgorithm- , messageDefaultChunkSize :: Word8- , messageS2KSaltOctets :: Int- , messageSEIPDv2SaltOctets :: Int- , messageDefaultS2KForSalt :: Salt -> S2K- , messageSEIPDv2SymmetricAlgorithms :: [SymmetricAlgorithm]- }+ { messageDefaultSymmetricAlgorithm :: SymmetricAlgorithm+ , messageDefaultAEADAlgorithm :: AEADAlgorithm+ , messageDefaultChunkSize :: Word8+ , messageS2KSaltOctets :: Int+ , messageSEIPDv2SaltOctets :: Int+ , messageDefaultS2KForSalt :: Salt -> S2K+ , messageSEIPDv2SymmetricAlgorithms :: [SymmetricAlgorithm]+ } data SecretKeyProtectionPolicy = SecretKeyProtectionPolicy- { secretKeyDefaultSymmetricAlgorithm :: SymmetricAlgorithm- , secretKeyDefaultAEADAlgorithm :: AEADAlgorithm- , secretKeyDefaultS2KForSalt :: Salt -> S2K- , secretKeyS2KSaltOctets :: Int- , secretKeyAEADNonceOctets :: Int- }+ { secretKeyDefaultSymmetricAlgorithm :: SymmetricAlgorithm+ , secretKeyDefaultAEADAlgorithm :: AEADAlgorithm+ , secretKeyDefaultS2KForSalt :: Salt -> S2K+ , secretKeyS2KSaltOctets :: Int+ , secretKeyAEADNonceOctets :: Int+ } data GenerationDeprecationPolicy = GenerationDeprecationPolicy- { deprecatedHashAlgorithms :: [HashAlgorithm]- , deprecatedSymmetricAlgorithms :: [SymmetricAlgorithm]- }+ { deprecatedHashAlgorithms :: [HashAlgorithm]+ , deprecatedSymmetricAlgorithms :: [SymmetricAlgorithm]+ } data VerificationDefaults = VerificationDefaults- { verificationDefaultStrict :: Bool- , verificationDefaultStreaming :: Bool- }+ { verificationDefaultStrict :: Bool+ , verificationDefaultStreaming :: Bool+ } --- | Per-message decrypt-side enforcement policy.------ 'defaultDecryptPolicy' applies RFC9580-strict rules: no unauthenticated--- (SED) ciphertext, modern symmetric algorithms only, and rejection of--- deprecated S2K specifiers in SKESK. Use 'lenientDecryptPolicy' when--- interoperating with older RFC4880 or RFC2440 messages.+-- | Action to take when a signature feature violates policy.+data VerificationPolicyAction+ = -- | Treat as a hard verification error (reject signature)+ VerificationError+ | -- | Accept signature but record a warning+ VerificationWarning+ deriving (Eq, Show)++{- | Policy controlling how signature verification handles deprecated or+discouraged features. This allows callers to choose between strict+rejection (errors) and lenient acceptance with warnings.+-}+data VerificationPolicy = VerificationPolicy+ { vpDeprecatedHashAlgorithm :: VerificationPolicyAction+ -- ^ Action for deprecated hash algorithms (MD5, SHA1, RIPEMD160)+ , vpUnsupportedHashAlgorithm :: VerificationPolicyAction+ -- ^ Action for unsupported/unknown hash algorithms+ , vpPkaMismatch :: VerificationPolicyAction+ -- ^ Action for PKA mismatch between signature and key+ , vpUnsupportedCriticalSubpacket :: VerificationPolicyAction+ -- ^ Action for unsupported critical subpackets+ , vpLegacyIssuerKeyIdInV6 :: VerificationPolicyAction+ -- ^ Action for legacy Issuer Key ID subpacket in v6 signatures+ , vpMissingSubkeyBackSignature :: VerificationPolicyAction+ -- ^ Action for missing subkey back-signature (v6 subkeys)+ , vpInvalidSignatureContext :: VerificationPolicyAction+ -- ^ Action for signature context violations (wrong sig type for context)+ , vpExpiredSignature :: VerificationPolicyAction+ -- ^ Action for expired signatures+ }+ deriving (Eq, Show)++{- | Default verification policy: strict on security-critical issues,+lenient on deprecated but still verifiable features.+-}+defaultVerificationPolicy :: VerificationPolicy+defaultVerificationPolicy =+ VerificationPolicy+ { vpDeprecatedHashAlgorithm = VerificationWarning+ , vpUnsupportedHashAlgorithm = VerificationError+ , vpPkaMismatch = VerificationError+ , vpUnsupportedCriticalSubpacket = VerificationError+ , vpLegacyIssuerKeyIdInV6 = VerificationError+ , vpMissingSubkeyBackSignature = VerificationWarning+ , vpInvalidSignatureContext = VerificationError+ , vpExpiredSignature = VerificationError+ }++-- | Strict verification policy: all policy violations are hard errors.+strictVerificationPolicy :: VerificationPolicy+strictVerificationPolicy =+ VerificationPolicy+ { vpDeprecatedHashAlgorithm = VerificationError+ , vpUnsupportedHashAlgorithm = VerificationError+ , vpPkaMismatch = VerificationError+ , vpUnsupportedCriticalSubpacket = VerificationError+ , vpLegacyIssuerKeyIdInV6 = VerificationError+ , vpMissingSubkeyBackSignature = VerificationError+ , vpInvalidSignatureContext = VerificationError+ , vpExpiredSignature = VerificationError+ }++{- | Lenient verification policy: all policy violations are warnings.+Use only for interoperability with legacy data.+-}+lenientVerificationPolicy :: VerificationPolicy+lenientVerificationPolicy =+ VerificationPolicy+ { vpDeprecatedHashAlgorithm = VerificationWarning+ , vpUnsupportedHashAlgorithm = VerificationWarning+ , vpPkaMismatch = VerificationWarning+ , vpUnsupportedCriticalSubpacket = VerificationWarning+ , vpLegacyIssuerKeyIdInV6 = VerificationWarning+ , vpMissingSubkeyBackSignature = VerificationWarning+ , vpInvalidSignatureContext = VerificationWarning+ , vpExpiredSignature = VerificationWarning+ }++-- | Check if a policy action is an error.+isVerificationError :: VerificationPolicyAction -> Bool+isVerificationError VerificationError = True+isVerificationError VerificationWarning = False++-- | Check if a policy action is a warning.+isVerificationWarning :: VerificationPolicyAction -> Bool+isVerificationWarning VerificationWarning = True+isVerificationWarning VerificationError = False++-- | Apply a policy action: return Left error or Right warning message.+applyVerificationPolicy+ :: VerificationPolicyAction+ -> String+ -> Either String String+applyVerificationPolicy action msg =+ if isVerificationError action+ then Left msg+ else Right msg++{- | Per-message decrypt-side enforcement policy.++'defaultDecryptPolicy' applies RFC9580-strict rules: no unauthenticated+(SED) ciphertext, modern symmetric algorithms only, and rejection of+deprecated S2K specifiers in SKESK. Use 'lenientDecryptPolicy' when+interoperating with older RFC4880 or RFC2440 messages.+-} data DecryptPolicy = DecryptPolicy- { -- | When 'False' (RFC9580 default), receiving a Symmetrically Encrypted- -- Data packet (SED, tag 9) is a hard error. RFC9580 §5.9 says- -- implementations SHOULD reject unauthenticated ciphertext.- decryptAllowSEDNoIntegrity :: Bool- -- | When 'False', receiving a SEIPDv1 (MDC-protected) packet is rejected.- -- Defaults to 'True' for interoperability with RFC4880 senders.- , decryptAllowSEIPDv1 :: Bool- -- | Allowed symmetric algorithms for session keys. 'Nothing' means- -- unrestricted. The RFC9580 default restricts to AES-128/192/256.- , decryptAllowedSymmetricAlgos :: Maybe [SymmetricAlgorithm]- -- | Allowed AEAD algorithms for SEIPDv2 payloads. 'Nothing' means- -- unrestricted. The RFC9580 default allows EAX, OCB, and GCM.- , decryptAllowedAEADAlgos :: Maybe [AEADAlgorithm]- -- | When 'True' (RFC9580 default), reject SKESK packets that use Simple- -- or Salted S2K specifiers (both deprecated since RFC9580).- , decryptRejectDeprecatedSKESK :: Bool- -- | When 'True' (RFC9580 default), any packet received after the- -- message-integrity boundary (MDC for SEIPDv1, final AEAD tag for- -- SEIPDv2, or the outer encrypted-data packet for SED) is a hard error.- -- RFC9580 §5.13.2 requires that implementations detect and reject- -- data appended after the authenticated payload. Set to 'False' only- -- when interoperating with legacy implementations that emit trailing- -- garbage (implies 'lenientDecryptPolicy').- , decryptRejectTrailingData :: Bool- -- | When 'True' (RFC9580 default), a payload with no version-aligned- -- ESK is a hard error even when misaligned ESKs are present. This is a- -- distinct concern from trailing-data rejection: a message could have- -- correct framing yet still carry only v6 PKESKs before a SEIPDv1- -- payload (or only v4 SKESKs before a SEIPDv2 payload), which indicates- -- a mis-assembled message rather than an integrity violation.- -- Set to 'False' only when salvaging malformed legacy messages.- , decryptRejectESKVersionMismatch :: Bool- } deriving (Eq, Show)+ { decryptAllowSEDNoIntegrity :: Bool+ {- ^ When 'False' (RFC9580 default), receiving a Symmetrically Encrypted+ Data packet (SED, tag 9) is a hard error. RFC9580 §5.9 says+ implementations SHOULD reject unauthenticated ciphertext.+ -}+ , decryptAllowSEIPDv1 :: Bool+ {- ^ When 'False', receiving a SEIPDv1 (MDC-protected) packet is rejected.+ Defaults to 'True' for interoperability with RFC4880 senders.+ -}+ , decryptAllowedSymmetricAlgos :: Maybe [SymmetricAlgorithm]+ {- ^ Allowed symmetric algorithms for session keys. 'Nothing' means+ unrestricted. The RFC9580 default restricts to AES-128/192/256.+ -}+ , decryptAllowedAEADAlgos :: Maybe [AEADAlgorithm]+ {- ^ Allowed AEAD algorithms for SEIPDv2 payloads. 'Nothing' means+ unrestricted. The RFC9580 default allows EAX, OCB, and GCM.+ -}+ , decryptRejectDeprecatedSKESK :: Bool+ {- ^ When 'True' (RFC9580 default), reject SKESK packets that use Simple+ or Salted S2K specifiers (both deprecated since RFC9580).+ -}+ , decryptRejectTrailingData :: Bool+ {- ^ When 'True' (RFC9580 default), any packet received after the+ message-integrity boundary (MDC for SEIPDv1, final AEAD tag for+ SEIPDv2, or the outer encrypted-data packet for SED) is a hard error.+ RFC9580 §5.13.2 requires that implementations detect and reject+ data appended after the authenticated payload. Set to 'False' only+ when interoperating with legacy implementations that emit trailing+ garbage (implies 'lenientDecryptPolicy').+ -}+ , decryptRejectESKVersionMismatch :: Bool+ {- ^ When 'True' (RFC9580 default), a payload with no version-aligned+ ESK is a hard error even when misaligned ESKs are present. This is a+ distinct concern from trailing-data rejection: a message could have+ correct framing yet still carry only v6 PKESKs before a SEIPDv1+ payload (or only v4 SKESKs before a SEIPDv2 payload), which indicates+ a mis-assembled message rather than an integrity violation.+ Set to 'False' only when salvaging malformed legacy messages.+ -}+ }+ deriving (Eq, Show) data OpenPGPPolicy = OpenPGPPolicy- { policyRFC :: OpenPGPRFC- , policyMessageEncryption :: MessageEncryptionPolicy- , policySecretKeyProtection :: Maybe SecretKeyProtectionPolicy- , policyGenerationDeprecations :: GenerationDeprecationPolicy- , policyDecrypt :: DecryptPolicy- }+ { policyRFC :: OpenPGPRFC+ , policyMessageEncryption :: MessageEncryptionPolicy+ , policySecretKeyProtection :: Maybe SecretKeyProtectionPolicy+ , policyGenerationDeprecations :: GenerationDeprecationPolicy+ , policyDecrypt :: DecryptPolicy+ } policyForRFC :: OpenPGPRFC -> OpenPGPPolicy policyForRFC RFC2440 =- OpenPGPPolicy- { policyRFC = RFC2440- , policyMessageEncryption =- MessageEncryptionPolicy- { messageDefaultSymmetricAlgorithm = TripleDES- , messageDefaultAEADAlgorithm = OCB- , messageDefaultChunkSize = 6- , messageS2KSaltOctets = 8- , messageSEIPDv2SaltOctets = 32- , messageDefaultS2KForSalt = \salt -> IteratedSalted SHA1 (requireSalt8 "RFC2440 message S2K" salt) 65536- , messageSEIPDv2SymmetricAlgorithms = []- }- , policySecretKeyProtection = Nothing- , policyGenerationDeprecations =- GenerationDeprecationPolicy- { deprecatedHashAlgorithms = []- , deprecatedSymmetricAlgorithms = []- }- , policyDecrypt = lenientDecryptPolicy- }+ OpenPGPPolicy+ { policyRFC = RFC2440+ , policyMessageEncryption =+ MessageEncryptionPolicy+ { messageDefaultSymmetricAlgorithm = TripleDES+ , messageDefaultAEADAlgorithm = OCB+ , messageDefaultChunkSize = 6+ , messageS2KSaltOctets = 8+ , messageSEIPDv2SaltOctets = 32+ , messageDefaultS2KForSalt = \salt ->+ IteratedSalted+ SHA1+ (requireSalt8 "RFC2440 message S2K" salt)+ 65536+ , messageSEIPDv2SymmetricAlgorithms = []+ }+ , policySecretKeyProtection = Nothing+ , policyGenerationDeprecations =+ GenerationDeprecationPolicy+ { deprecatedHashAlgorithms = []+ , deprecatedSymmetricAlgorithms = []+ }+ , policyDecrypt = lenientDecryptPolicy+ } policyForRFC RFC4880 =- OpenPGPPolicy- { policyRFC = RFC4880- , policyMessageEncryption =- MessageEncryptionPolicy- { messageDefaultSymmetricAlgorithm = AES128- , messageDefaultAEADAlgorithm = OCB- , messageDefaultChunkSize = 6- , messageS2KSaltOctets = 8- , messageSEIPDv2SaltOctets = 32- , messageDefaultS2KForSalt = \salt -> IteratedSalted SHA256 (requireSalt8 "RFC4880 message S2K" salt) 65536- , messageSEIPDv2SymmetricAlgorithms = []- }- , policySecretKeyProtection = Nothing- , policyGenerationDeprecations =- GenerationDeprecationPolicy- { deprecatedHashAlgorithms = [DeprecatedMD5]- , deprecatedSymmetricAlgorithms = []- }- , policyDecrypt = lenientDecryptPolicy- }+ OpenPGPPolicy+ { policyRFC = RFC4880+ , policyMessageEncryption =+ MessageEncryptionPolicy+ { messageDefaultSymmetricAlgorithm = AES128+ , messageDefaultAEADAlgorithm = OCB+ , messageDefaultChunkSize = 6+ , messageS2KSaltOctets = 8+ , messageSEIPDv2SaltOctets = 32+ , messageDefaultS2KForSalt = \salt ->+ IteratedSalted+ SHA256+ (requireSalt8 "RFC4880 message S2K" salt)+ 65536+ , messageSEIPDv2SymmetricAlgorithms = []+ }+ , policySecretKeyProtection = Nothing+ , policyGenerationDeprecations =+ GenerationDeprecationPolicy+ { deprecatedHashAlgorithms = [DeprecatedMD5]+ , deprecatedSymmetricAlgorithms = []+ }+ , policyDecrypt = lenientDecryptPolicy+ } policyForRFC RFC9580 =- OpenPGPPolicy- { policyRFC = RFC9580- , policyMessageEncryption =- MessageEncryptionPolicy- { messageDefaultSymmetricAlgorithm = AES256- , messageDefaultAEADAlgorithm = OCB- , messageDefaultChunkSize = 6- , messageS2KSaltOctets = 16- , messageSEIPDv2SaltOctets = 32- , messageDefaultS2KForSalt = \salt -> Argon2 (requireSalt16 "RFC9580 message S2K" salt) 1 4 15- , messageSEIPDv2SymmetricAlgorithms = [AES128, AES192, AES256]- }- , policySecretKeyProtection =- Just- SecretKeyProtectionPolicy- { secretKeyDefaultSymmetricAlgorithm = AES256- , secretKeyDefaultAEADAlgorithm = OCB- , secretKeyDefaultS2KForSalt = \salt -> Argon2 (requireSalt16 "RFC9580 secret key S2K" salt) 1 4 15- , secretKeyS2KSaltOctets = 16- , secretKeyAEADNonceOctets = 15- }- , policyGenerationDeprecations =- GenerationDeprecationPolicy- { deprecatedHashAlgorithms = [DeprecatedMD5, SHA1, RIPEMD160]- , deprecatedSymmetricAlgorithms = [IDEA, TripleDES, CAST5, Blowfish]- }- , policyDecrypt = defaultDecryptPolicy- }+ OpenPGPPolicy+ { policyRFC = RFC9580+ , policyMessageEncryption =+ MessageEncryptionPolicy+ { messageDefaultSymmetricAlgorithm = AES256+ , messageDefaultAEADAlgorithm = OCB+ , messageDefaultChunkSize = 6+ , messageS2KSaltOctets = 16+ , messageSEIPDv2SaltOctets = 32+ , messageDefaultS2KForSalt = \salt -> Argon2 (requireSalt16 "RFC9580 message S2K" salt) 1 4 15+ , messageSEIPDv2SymmetricAlgorithms = [AES128, AES192, AES256]+ }+ , policySecretKeyProtection =+ Just+ SecretKeyProtectionPolicy+ { secretKeyDefaultSymmetricAlgorithm = AES256+ , secretKeyDefaultAEADAlgorithm = OCB+ , secretKeyDefaultS2KForSalt = \salt -> Argon2 (requireSalt16 "RFC9580 secret key S2K" salt) 1 4 15+ , secretKeyS2KSaltOctets = 16+ , secretKeyAEADNonceOctets = 15+ }+ , policyGenerationDeprecations =+ GenerationDeprecationPolicy+ { deprecatedHashAlgorithms = [DeprecatedMD5, SHA1, RIPEMD160]+ , deprecatedSymmetricAlgorithms =+ [IDEA, TripleDES, CAST5, Blowfish]+ }+ , policyDecrypt = defaultDecryptPolicy+ } requireSalt8 :: String -> Salt -> Salt8 requireSalt8 context salt =- case salt8FromSalt salt of- Just salt8 -> salt8- Nothing -> error (context ++ " requires an 8-octet salt")+ case salt8FromSalt salt of+ Just salt8 -> salt8+ Nothing -> error (context ++ " requires an 8-octet salt") requireSalt16 :: String -> Salt -> Salt16 requireSalt16 context salt =- case salt16FromSalt salt of- Just salt16 -> salt16- Nothing -> error (context ++ " requires a 16-octet salt")+ case salt16FromSalt salt of+ Just salt16 -> salt16+ Nothing -> error (context ++ " requires a 16-octet salt") defaultPolicy :: OpenPGPPolicy defaultPolicy = policyForRFC RFC9580@@ -339,51 +482,58 @@ defaultVerificationDefaults :: VerificationDefaults defaultVerificationDefaults =- VerificationDefaults- { verificationDefaultStrict = True- , verificationDefaultStreaming = True- }+ VerificationDefaults+ { verificationDefaultStrict = True+ , verificationDefaultStreaming = True+ } --- | RFC9580-strict decrypt policy. Rejects unauthenticated SED ciphertext,--- restricts session-key symmetric algorithms to AES-128/192/256 and AEAD to--- EAX/OCB/GCM, and rejects SKESK packets carrying deprecated Simple or--- Salted S2K specifiers. SEIPDv1 (MDC-protected) is still accepted for--- interoperability with RFC4880 senders.+{- | RFC9580-strict decrypt policy. Rejects unauthenticated SED ciphertext,+restricts session-key symmetric algorithms to AES-128/192/256 and AEAD to+EAX/OCB/GCM, and rejects SKESK packets carrying deprecated Simple or+Salted S2K specifiers. SEIPDv1 (MDC-protected) is still accepted for+interoperability with RFC4880 senders.+-} defaultDecryptPolicy :: DecryptPolicy defaultDecryptPolicy =- DecryptPolicy- { decryptAllowSEDNoIntegrity = False- , decryptAllowSEIPDv1 = True- , decryptAllowedSymmetricAlgos = Just [AES128, AES192, AES256]- , decryptAllowedAEADAlgos = Just [EAX, OCB, GCM]- , decryptRejectDeprecatedSKESK = True- , decryptRejectTrailingData = True- , decryptRejectESKVersionMismatch = True- }+ DecryptPolicy+ { decryptAllowSEDNoIntegrity = False+ , decryptAllowSEIPDv1 = True+ , decryptAllowedSymmetricAlgos = Just [AES128, AES192, AES256]+ , decryptAllowedAEADAlgos = Just [EAX, OCB, GCM]+ , decryptRejectDeprecatedSKESK = True+ , decryptRejectTrailingData = True+ , decryptRejectESKVersionMismatch = True+ } --- | Permissive decrypt policy for interoperability with RFC4880 and RFC2440--- messages. No algorithm or integrity restrictions are applied.+{- | Permissive decrypt policy for interoperability with RFC4880 and RFC2440+messages. No algorithm or integrity restrictions are applied.+-} lenientDecryptPolicy :: DecryptPolicy lenientDecryptPolicy =- DecryptPolicy- { decryptAllowSEDNoIntegrity = True- , decryptAllowSEIPDv1 = True- , decryptAllowedSymmetricAlgos = Nothing- , decryptAllowedAEADAlgos = Nothing- , decryptRejectDeprecatedSKESK = False- , decryptRejectTrailingData = False- , decryptRejectESKVersionMismatch = False- }+ DecryptPolicy+ { decryptAllowSEDNoIntegrity = True+ , decryptAllowSEIPDv1 = True+ , decryptAllowedSymmetricAlgos = Nothing+ , decryptAllowedAEADAlgos = Nothing+ , decryptRejectDeprecatedSKESK = False+ , decryptRejectTrailingData = False+ , decryptRejectESKVersionMismatch = False+ } -supportsSEIPDv2Symmetric :: OpenPGPPolicy -> SymmetricAlgorithm -> Bool+supportsSEIPDv2Symmetric+ :: OpenPGPPolicy -> SymmetricAlgorithm -> Bool supportsSEIPDv2Symmetric policy sa =- sa `elem` messageSEIPDv2SymmetricAlgorithms (policyMessageEncryption policy)+ sa+ `elem` messageSEIPDv2SymmetricAlgorithms+ (policyMessageEncryption policy) -secretKeyProtectionPolicyForKeyVersion :: OpenPGPPolicy -> KeyVersion -> Maybe SecretKeyProtectionPolicy+secretKeyProtectionPolicyForKeyVersion+ :: OpenPGPPolicy -> KeyVersion -> Maybe SecretKeyProtectionPolicy secretKeyProtectionPolicyForKeyVersion policy V6 = policySecretKeyProtection policy secretKeyProtectionPolicyForKeyVersion _ _ = Nothing -ecdhKdfHashDigest :: HashAlgorithm -> B.ByteString -> Either String B.ByteString+ecdhKdfHashDigest+ :: HashAlgorithm -> B.ByteString -> Either String B.ByteString ecdhKdfHashDigest SHA1 _ = Left "ECDH KDF hash algorithm SHA1 is disallowed by policy" ecdhKdfHashDigest SHA224 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHAlg.SHA224)) ecdhKdfHashDigest SHA256 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHAlg.SHA256))@@ -393,68 +543,74 @@ ecdhKdfHashDigest SHA3_512 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHAlg.SHA3_512)) ecdhKdfHashDigest _ _ = Left "ECDH KDF hash algorithm is unsupported" -validateTable30PolicyForRecipient ::- SomePKPayload -> HashAlgorithm -> SymmetricAlgorithm -> Either String ()+validateTable30PolicyForRecipient+ :: SomePKPayload+ -> HashAlgorithm+ -> SymmetricAlgorithm+ -> Either String () validateTable30PolicyForRecipient recipientPKP kdfHA kdfSA =- case allowedTable30EcdhParameterSets recipientPKP (_pubkey recipientPKP) of- Nothing -> Right ()- Just (curveName, allowedParams) ->- if (kdfHA, kdfSA) `elem` allowedParams- then Right ()- else- Left- ("RFC9580 Table 30 policy violation for " ++- show (_keyVersion recipientPKP) ++- " ECDH key on " ++- curveName ++- ": expected one of " ++- show allowedParams ++- ", got (" ++- show kdfHA ++- ", " ++- show kdfSA ++- ")")+ case allowedTable30EcdhParameterSets+ recipientPKP+ (_pubkey recipientPKP) of+ Nothing -> Right ()+ Just (curveName, allowedParams) ->+ if (kdfHA, kdfSA) `elem` allowedParams+ then Right ()+ else+ Left+ ( "RFC9580 Table 30 policy violation for "+ ++ show (_keyVersion recipientPKP)+ ++ " ECDH key on "+ ++ curveName+ ++ ": expected one of "+ ++ show allowedParams+ ++ ", got ("+ ++ show kdfHA+ ++ ", "+ ++ show kdfSA+ ++ ")"+ ) -allowedTable30EcdhParameterSets ::- SomePKPayload- -> PKey- -> Maybe (String, [(HashAlgorithm, SymmetricAlgorithm)])+allowedTable30EcdhParameterSets+ :: SomePKPayload+ -> PKey+ -> Maybe (String, [(HashAlgorithm, SymmetricAlgorithm)]) allowedTable30EcdhParameterSets recipientPKP (ECDHPubKey ecdhPub _ _)- | _keyVersion recipientPKP == V4 =- case ecdhPub of- EdDSAPubKey Ed25519 _ ->- Just- ( "Curve25519Legacy"- , [ (ha, sa)- | ha <- [SHA256, SHA384, SHA512]- , sa <- [AES128, AES192, AES256]- ]- )- ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _))- | curve == ECCT.getCurveByName ECCT.SEC_p256r1 ->- Just ("NIST P-256", [(SHA256, AES128)])- | curve == ECCT.getCurveByName ECCT.SEC_p384r1 ->- Just ("NIST P-384", [(SHA384, AES192)])- | curve == ECCT.getCurveByName ECCT.SEC_p521r1 ->- Just ("NIST P-521", [(SHA512, AES256)])- _ -> Nothing- | _keyVersion recipientPKP == V6 =- case ecdhPub of- EdDSAPubKey Ed25519 _ -> Just ("Curve25519Legacy", [(SHA256, AES128)])- ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _))- | curve == ECCT.getCurveByName ECCT.SEC_p256r1 ->- Just ("NIST P-256", [(SHA256, AES128)])- | curve == ECCT.getCurveByName ECCT.SEC_p384r1 ->- Just ("NIST P-384", [(SHA384, AES192)])- | curve == ECCT.getCurveByName ECCT.SEC_p521r1 ->- Just ("NIST P-521", [(SHA512, AES256)])- _ -> Nothing- | otherwise = Nothing+ | _keyVersion recipientPKP == V4 =+ case ecdhPub of+ EdDSAPubKey EdSigningCurve25519 _ ->+ Just+ ( "Curve25519Legacy"+ , [ (ha, sa)+ | ha <- [SHA256, SHA384, SHA512]+ , sa <- [AES128, AES192, AES256]+ ]+ )+ ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _))+ | curve == ECCT.getCurveByName ECCT.SEC_p256r1 ->+ Just ("NIST P-256", [(SHA256, AES128)])+ | curve == ECCT.getCurveByName ECCT.SEC_p384r1 ->+ Just ("NIST P-384", [(SHA384, AES192)])+ | curve == ECCT.getCurveByName ECCT.SEC_p521r1 ->+ Just ("NIST P-521", [(SHA512, AES256)])+ _ -> Nothing+ | _keyVersion recipientPKP == V6 =+ case ecdhPub of+ EdDSAPubKey EdSigningCurve25519 _ -> Just ("Curve25519Legacy", [(SHA256, AES128)])+ ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _))+ | curve == ECCT.getCurveByName ECCT.SEC_p256r1 ->+ Just ("NIST P-256", [(SHA256, AES128)])+ | curve == ECCT.getCurveByName ECCT.SEC_p384r1 ->+ Just ("NIST P-384", [(SHA384, AES192)])+ | curve == ECCT.getCurveByName ECCT.SEC_p521r1 ->+ Just ("NIST P-521", [(SHA512, AES256)])+ _ -> Nothing+ | otherwise = Nothing allowedTable30EcdhParameterSets _ _ = Nothing legacySecretKeyProtectionErrorMessage :: String legacySecretKeyProtectionErrorMessage =- "re-encrypting legacy secret keys without SHA-1 protection is unsupported; explicit legacy override required"+ "re-encrypting legacy secret keys without SHA-1 protection is unsupported; explicit legacy override required" -- RFC9580/RFC4880 signature context validation predicates -- These enforce which signature types are allowed in which structural contexts
Codec/Encryption/OpenPGP/Serialize.hs view
@@ -2,3130 +2,3620 @@ -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).--{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE FlexibleInstances #-}--module Codec.Encryption.OpenPGP.Serialize- (- -- * Serialization functions- putPkt- , putPktEither- , putSKAddendum- , getSecretKey- , putSKeyForPKPayload- -- * Utilities- , dearmorIfAsciiArmored- , dearmorIfAsciiArmoredLenient- , looksLikeAsciiArmor- , armorPayloadsOfType- , singleArmorPayloadOfType- , singleClearSignedBlock- , recommendedArmorType- , WireRepInput(..)- , wireRepRefFromInput- , PktParseError(..)- , parsePkts- , parsePktsEither- , parsePktsWithWireRep- , conduitParsePktsWithWireRep- ) where--import Control.Applicative (many, some)-import Control.Arrow ((***))-import Control.Lens ((^.), _1)-import Control.Monad (guard, replicateM, replicateM_, when)-import Crypto.Number.Basic (numBits)-import Crypto.Number.ModArithmetic (inverse)-import Crypto.Number.Serialize (i2osp, os2ip)-import qualified Crypto.PubKey.DSA as D-import qualified Crypto.PubKey.ECC.ECDSA as ECDSA-import qualified Crypto.PubKey.ECC.Types as ECCT-import qualified Crypto.PubKey.RSA as R-import Data.Bifunctor (bimap)-import Data.Binary (Binary, get, put)-import Data.Binary.Get- ( ByteOffset- , Get- , bytesRead- , getByteString- , getLazyByteString- , getRemainingLazyByteString- , getWord16be- , getWord16le- , getWord32be- , getWord8- , lookAhead- , runGetOrFail- )-import Data.Binary.Put- ( Put- , putByteString- , putLazyByteString- , putWord16be- , putWord16le- , putWord32be- , putWord8- , runPut- )-import Data.Bits ((.&.), (.|.), shiftL, shiftR, testBit)-import qualified Data.ByteString as B-import Data.ByteString.Lazy (ByteString)-import qualified Data.ByteString.Lazy as BL-import qualified Data.ByteString.Lazy.Char8 as BLC8-import qualified Data.Foldable as F-import Data.Int (Int64)-import Data.List (mapAccumL)-import qualified Data.List.NonEmpty as NE-import Data.Maybe (fromMaybe)-import Data.Set (Set)-import qualified Data.Set as Set-import qualified Data.Text as T-import Data.Text.Encoding (decodeUtf8With, encodeUtf8)-import Data.Text.Encoding.Error (lenientDecode)-import Data.Word (Word16, Word32, Word8)-import Network.URI (nullURI, parseURI, uriToString)--import Codec.Encryption.OpenPGP.Internal- ( curve2Curve- , curveFromCurve- , curveToCurveoidBS- , curveoidBSToCurve- , curveoidBSToEdSigningCurve- , edSigningCurveToCurveoidBS- , leftPadTo- , pubkeyToMPIs- )-import Codec.Encryption.OpenPGP.Policy (signatureV6SaltSizeForHashAlgorithm)-import Codec.Encryption.OpenPGP.Types-import qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA-import Codec.Encryption.OpenPGP.ASCIIArmor.Types (Armor(..), ArmorType(..))-import Data.Conduit (ConduitT, await, yield)-import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as BTypes-import qualified Codec.Encryption.OpenPGP.Types.Internal.PKITypes as P--instance Binary SigSubPacket where- get = getSigSubPacket- put = putSigSubPacket---- instance Binary (Set NotationFlag) where--- put = putNotationFlagSet-instance Binary CompressionAlgorithm where- get = toFVal <$> getWord8- put = putWord8 . fromFVal--instance Binary PubKeyAlgorithm where- get = toFVal <$> getWord8- put = putWord8 . fromFVal--instance Binary HashAlgorithm where- get = toFVal <$> getWord8- put = putWord8 . fromFVal--instance Binary SymmetricAlgorithm where- get = toFVal <$> getWord8- put = putWord8 . fromFVal--instance Binary MPI where- get = getMPI- put = putMPI--instance Binary SigType where- get = toFVal <$> getWord8- put = putWord8 . fromFVal--instance Binary UserAttrSubPacket where- get = getUserAttrSubPacket- put = putUserAttrSubPacket--instance Binary S2K where- get = getS2K- put = putS2K--instance Binary (PKESK 'PKESKV3) where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary (PKESK 'PKESKV6) where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary Signature where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary (SKESK 'SKESKV4) where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary (SKESK 'SKESKV6) where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary (OnePassSignature 'OPSV3) where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary (OnePassSignature 'OPSV6) where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary SecretKey where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary PublicKey where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary SecretSubkey where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary CompressedData where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary SymEncData where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary Marker where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary LiteralData where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary Trust where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary UserId where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary PublicSubkey where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary UserAttribute where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary SymEncIntegrityProtectedData where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary ModificationDetectionCode where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary OtherPacket where- get = getPkt >>= either fail pure . fromPktEither- put = putPkt . toPkt--instance Binary Pkt where- get = getPkt- put = putPkt--instance Binary a => Binary (Block a) where- get = Block `fmap` many get- put = mapM_ put . unBlock--instance Binary SomePKPayload where- get = getPKPayload- put = putPKPayload--instance Binary SignaturePayload where- get = getSignaturePayload- put = putSignaturePayload--instance Binary TKUnknown where- get = fail "Binary TKUnknown decode is not implemented"- put = putTK--getSigSubPacket :: Get SigSubPacket-getSigSubPacket = do- l <- fmap fromIntegral getSubPacketLength- (crit, pt) <- getSigSubPacketType- getSigSubPacket' pt crit l- where- getSigSubPacket' :: Word8 -> Bool -> ByteOffset -> Get SigSubPacket- getSigSubPacket' pt crit l- | pt == 2 = do- et <- fmap ThirtyTwoBitTimeStamp getWord32be- return $ SigSubPacket crit (SigCreationTime et)- | pt == 3 = do- et <- fmap ThirtyTwoBitDuration getWord32be- return $ SigSubPacket crit (SigExpirationTime et)- | pt == 4 = do- e <- get- return $ SigSubPacket crit (ExportableCertification e)- | pt == 5 = do- tl <- getWord8- ta <- getWord8- return $ SigSubPacket crit (TrustSignature tl ta)- | pt == 6 = do- apdre <- getLazyByteString (l - 2)- nul <- getWord8- guard (nul == 0)- return $ SigSubPacket crit (RegularExpression (BL.copy apdre))- | pt == 7 = do- r <- get- return $ SigSubPacket crit (Revocable r)- | pt == 9 = do- et <- fmap ThirtyTwoBitDuration getWord32be- return $ SigSubPacket crit (KeyExpirationTime et)- | pt == 11 = do- sa <- replicateM (fromIntegral (l - 1)) get- return $ SigSubPacket crit (PreferredSymmetricAlgorithms sa)- | pt == 12 = do- rclass <- getWord8- guard (testBit rclass 7)- algid <- get- fp <- getLazyByteString (fromIntegral l - 3)- return $- SigSubPacket- crit- (RevocationKey- (bsToFFSet . BL.singleton $ rclass .&. 0x7f)- algid- (Fingerprint fp))- | pt == 16 = do- keyid <- getLazyByteString (l - 1)- return $ SigSubPacket crit (Issuer (EightOctetKeyId keyid))- | pt == 20 = do- flags <- getLazyByteString 4- nl <- getWord16be- vl <- getWord16be- nn <- getLazyByteString (fromIntegral nl)- nv <- getLazyByteString (fromIntegral vl)- return $- SigSubPacket- crit- (NotationData (bsToFFSet flags) (NotationName nn) (NotationValue nv))- | pt == 21 = do- ha <- replicateM (fromIntegral (l - 1)) get- return $ SigSubPacket crit (PreferredHashAlgorithms ha)- | pt == 22 = do- ca <- replicateM (fromIntegral (l - 1)) get- return $ SigSubPacket crit (PreferredCompressionAlgorithms ca)- | pt == 23 = do- ksps <- getLazyByteString (l - 1)- return $ SigSubPacket crit (KeyServerPreferences (bsToFFSet ksps))- | pt == 24 = do- pks <- getLazyByteString (l - 1)- return $ SigSubPacket crit (PreferredKeyServer pks)- | pt == 25 = do- primacy <- get- return $ SigSubPacket crit (PrimaryUserId primacy)- | pt == 26 = do- url <-- fmap- (URL . fromMaybe nullURI . parseURI . T.unpack .- decodeUtf8With lenientDecode)- (getByteString (fromIntegral (l - 1)))- return $ SigSubPacket crit (PolicyURL url)- | pt == 27 = do- kfs <- getLazyByteString (l - 1)- return $ SigSubPacket crit (KeyFlags (bsToFFSet kfs))- | pt == 28 = do- uid <- getByteString (fromIntegral (l - 1))- return $- SigSubPacket crit (SignersUserId (decodeUtf8With lenientDecode uid))- | pt == 29 = do- rcode <- getWord8- rreason <-- fmap- (decodeUtf8With lenientDecode)- (getByteString (fromIntegral (l - 2)))- return $ SigSubPacket crit (ReasonForRevocation (toFVal rcode) rreason)- | pt == 30 = do- fbs <- getLazyByteString (l - 1)- return $ SigSubPacket crit (Features (bsToFFSet fbs))- | pt == 31 = do- pka <- get- ha <- get- hash <- getLazyByteString (l - 3)- return $ SigSubPacket crit (SignatureTarget pka ha hash)- | pt == 32 = do- spbs <- getLazyByteString (l - 1)- case runGetOrFail get spbs of- Left (_, _, e) -> fail ("embedded signature subpacket " ++ e)- Right (_, _, sp) -> return $ SigSubPacket crit (EmbeddedSignature sp)- | pt == 33 = do- when (l /= 22 && l /= 34) $- fail ("invalid issuer fingerprint subpacket length: " ++ show l)- kv <- getWord8- let fpLen = l - 2- when (fpLen /= 20 && fpLen /= 32) $- fail ("invalid issuer fingerprint length: " ++ show fpLen)- case BTypes.packetVersionToIssuerFingerprintVersion kv of- Nothing ->- fail ("invalid issuer fingerprint version marker: " ++ show kv)- Just ifVersion -> do- fp <-- case kv of- 4 -> getLazyByteString (fromIntegral fpLen)- 6 -> getLazyByteString (fromIntegral fpLen)- _ -> fail ("invalid issuer fingerprint version marker: " ++ show kv)- return $- SigSubPacket crit (IssuerFingerprint ifVersion (Fingerprint fp))- | pt > 99 && pt < 111 = do- payload <- getLazyByteString (l - 1)- return $ SigSubPacket crit (UserDefinedSigSub pt payload)- | otherwise = do- payload <- getLazyByteString (l - 1)- return $ SigSubPacket crit (OtherSigSub pt payload)--putSigSubPacket :: SigSubPacket -> Put-putSigSubPacket (SigSubPacket crit (SigCreationTime et)) = do- putSubPacketLength 5- putSigSubPacketType crit 2- putWord32be . unThirtyTwoBitTimeStamp $ et-putSigSubPacket (SigSubPacket crit (SigExpirationTime et)) = do- putSubPacketLength 5- putSigSubPacketType crit 3- putWord32be . unThirtyTwoBitDuration $ et-putSigSubPacket (SigSubPacket crit (ExportableCertification e)) = do- putSubPacketLength 2- putSigSubPacketType crit 4- put e-putSigSubPacket (SigSubPacket crit (TrustSignature tl ta)) = do- putSubPacketLength 3- putSigSubPacketType crit 5- put tl- put ta-putSigSubPacket (SigSubPacket crit (RegularExpression apdre)) = do- putSubPacketLength . fromIntegral $ (2 + BL.length apdre)- putSigSubPacketType crit 6- putLazyByteString apdre- putWord8 0-putSigSubPacket (SigSubPacket crit (Revocable r)) = do- putSubPacketLength 2- putSigSubPacketType crit 7- put r-putSigSubPacket (SigSubPacket crit (KeyExpirationTime et)) = do- putSubPacketLength 5- putSigSubPacketType crit 9- putWord32be . unThirtyTwoBitDuration $ et-putSigSubPacket (SigSubPacket crit (PreferredSymmetricAlgorithms ess)) = do- putSubPacketLength . fromIntegral $ (1 + length ess)- putSigSubPacketType crit 11- mapM_ put ess-putSigSubPacket (SigSubPacket crit (RevocationKey rclass algid fp)) = do- let fpLen = BL.length (unFingerprint fp)- putSubPacketLength (fromIntegral (3 + fpLen)) -- type(1) + rclass(1) + algid(1) + fingerprint- putSigSubPacketType crit 12- putLazyByteString . ffSetToFixedLengthBS (1 :: Int) $- Set.insert (RClOther 0) rclass- put algid- putLazyByteString (unFingerprint fp)-putSigSubPacket (SigSubPacket crit (Issuer keyid)) = do- putSubPacketLength 9- putSigSubPacketType crit 16- putLazyByteString (unEOKI keyid) -- 8 octets-putSigSubPacket (SigSubPacket crit (NotationData nfs (NotationName nn) (NotationValue nv))) = do- putSubPacketLength . fromIntegral $ (9 + BL.length nn + BL.length nv)- putSigSubPacketType crit 20- putLazyByteString . ffSetToFixedLengthBS (4 :: Int) $ nfs- putWord16be . fromIntegral . BL.length $ nn- putWord16be . fromIntegral . BL.length $ nv- putLazyByteString nn- putLazyByteString nv-putSigSubPacket (SigSubPacket crit (PreferredHashAlgorithms ehs)) = do- putSubPacketLength . fromIntegral $ (1 + length ehs)- putSigSubPacketType crit 21- mapM_ put ehs-putSigSubPacket (SigSubPacket crit (PreferredCompressionAlgorithms ecs)) = do- putSubPacketLength . fromIntegral $ (1 + length ecs)- putSigSubPacketType crit 22- mapM_ put ecs-putSigSubPacket (SigSubPacket crit (KeyServerPreferences ksps)) = do- let kbs = ffSetToBS ksps- putSubPacketLength . fromIntegral $ (1 + BL.length kbs)- putSigSubPacketType crit 23- putLazyByteString kbs-putSigSubPacket (SigSubPacket crit (PreferredKeyServer ks)) = do- putSubPacketLength . fromIntegral $ (1 + BL.length ks)- putSigSubPacketType crit 24- putLazyByteString ks-putSigSubPacket (SigSubPacket crit (PrimaryUserId primacy)) = do- putSubPacketLength 2- putSigSubPacketType crit 25- put primacy-putSigSubPacket (SigSubPacket crit (PolicyURL (URL uri))) = do- let bs = encodeUtf8 (T.pack (uriToString id uri ""))- putSubPacketLength . fromIntegral $ (1 + B.length bs)- putSigSubPacketType crit 26- putByteString bs-putSigSubPacket (SigSubPacket crit (KeyFlags kfs)) = do- let kbs = ffSetToBS kfs- putSubPacketLength . fromIntegral $ (1 + BL.length kbs)- putSigSubPacketType crit 27- putLazyByteString kbs-putSigSubPacket (SigSubPacket crit (SignersUserId userid)) = do- let bs = encodeUtf8 userid- putSubPacketLength . fromIntegral $ (1 + B.length bs)- putSigSubPacketType crit 28- putByteString bs-putSigSubPacket (SigSubPacket crit (ReasonForRevocation rcode rreason)) = do- let reasonbs = encodeUtf8 rreason- putSubPacketLength . fromIntegral $ (2 + B.length reasonbs)- putSigSubPacketType crit 29- putWord8 . fromFVal $ rcode- putByteString reasonbs-putSigSubPacket (SigSubPacket crit (Features fs)) = do- let fbs = ffSetToBS fs- putSubPacketLength . fromIntegral $ (1 + BL.length fbs)- putSigSubPacketType crit 30- putLazyByteString fbs-putSigSubPacket (SigSubPacket crit (SignatureTarget pka ha hash)) = do- putSubPacketLength . fromIntegral $ (3 + BL.length hash)- putSigSubPacketType crit 31- put pka- put ha- putLazyByteString hash-putSigSubPacket (SigSubPacket crit (EmbeddedSignature sp)) = do- let spb = runPut (put sp)- putSubPacketLength . fromIntegral $ (1 + BL.length spb)- putSigSubPacketType crit 32- putLazyByteString spb-putSigSubPacket (SigSubPacket crit (IssuerFingerprint kv fp)) = do- let kv' = BTypes.issuerFingerprintVersionToPacketVersion kv- let fpb = unFingerprint fp- when (BL.length fpb /= 20 && BL.length fpb /= 32) $- error ("invalid issuer fingerprint length: " ++ show (BL.length fpb))- putSubPacketLength . fromIntegral $ (2 + BL.length fpb)- putSigSubPacketType crit 33- putWord8 kv'- putLazyByteString fpb-putSigSubPacket (SigSubPacket crit (UserDefinedSigSub ptype payload)) =- putSigSubPacket (SigSubPacket crit (OtherSigSub ptype payload))-putSigSubPacket (SigSubPacket crit (OtherSigSub ptype payload)) = do- putSubPacketLength . fromIntegral $ (1 + BL.length payload)- putSigSubPacketType crit ptype- putLazyByteString payload--getSubPacketLength :: Get Word32-getSubPacketLength = getSubPacketLength' =<< getWord8- where- getSubPacketLength' :: Integral a => Word8 -> Get a- getSubPacketLength' f- | f < 192 = return . fromIntegral $ f- | f < 224 = do- secondOctet <- getWord8- return . fromIntegral $ shiftL (fromIntegral (f - 192) :: Int) 8 +- (fromIntegral secondOctet :: Int) +- 192- | f == 255 = do- len <- getWord32be- return . fromIntegral $ len- | otherwise = fail "Partial body length invalid."--putSubPacketLength :: Word32 -> Put-putSubPacketLength l- | l < 192 = putWord8 (fromIntegral l)- | l < 8384 =- putWord8 (fromIntegral ((fromIntegral (l - 192) `shiftR` 8) + 192 :: Int)) >>- putWord8 (fromIntegral (l - 192) .&. 0xff)- | l <= 0xffffffff = putWord8 255 >> putWord32be (fromIntegral l)- | otherwise = error ("too big (" ++ show l ++ ")")--getSigSubPacketType :: Get (Bool, Word8)-getSigSubPacketType = do- x <- getWord8- return- (if x .&. 128 == 128- then (True, x .&. 127)- else (False, x))--putSigSubPacketType :: Bool -> Word8 -> Put-putSigSubPacketType False sst = putWord8 sst-putSigSubPacketType True sst = putWord8 (sst .|. 0x80)--bsToFFSet :: FutureFlag a => ByteString -> Set a-bsToFFSet bs =- Set.fromAscList . concat . snd $- mapAccumL- (\acc y -> (acc + 8, concatMap (shifty acc y) [0 .. 7]))- 0- (BL.unpack bs)- where- shifty acc y x = [toFFlag (acc + x) | y .&. shiftR 128 x == shiftR 128 x]--ffSetToFixedLengthBS :: (Integral a, FutureFlag b) => a -> Set b -> ByteString-ffSetToFixedLengthBS len ffs =- BL.take- (fromIntegral len)- (BL.append (ffSetToBS ffs) (BL.pack (replicate 5 0)))--ffSetToBS :: FutureFlag a => Set a -> ByteString-ffSetToBS = BL.pack . ffSetToBS'- where- ffSetToBS' :: FutureFlag a => Set a -> [Word8]- ffSetToBS' ks- -- Emit a single zero octet for an empty flag set so encoded flag- -- subpackets always carry an explicit flags byte.- | Set.null ks = [0]- | otherwise =- map- ((foldl (.|.) 0 . map (shiftR 128 . flip mod 8 . fromFFlag) .- Set.toAscList) .- (\x -> Set.filter (\y -> fromFFlag y `div` 8 == x) ks))- [0 .. fromFFlag (Set.findMax ks) `div` 8]--fromS2K :: S2K -> ByteString-fromS2K (Simple hashalgo) = BL.pack [0, fromIntegral . fromFVal $ hashalgo]-fromS2K (Salted hashalgo salt) =- BL.pack [1, fromIntegral . fromFVal $ hashalgo] `BL.append`- (BL.fromStrict . unSalt8) salt-fromS2K (IteratedSalted hashalgo salt count) =- BL.pack [3, fromIntegral . fromFVal $ hashalgo] `BL.append`- (BL.fromStrict . unSalt8) salt `BL.snoc`- encodeIterationCount count-fromS2K (Argon2 salt t p encodedM) =- BL.pack [4] `BL.append` (BL.fromStrict . unSalt16) salt `BL.append`- BL.pack [t, p, encodedM]-fromS2K (OtherS2K _ bs) = bs--getPacketLength :: Get Integer-getPacketLength = do- firstOctet <- getWord8- lenOrPartial <- lengthOctetToLength firstOctet- case lenOrPartial of- Left _ ->- fail "Partial body length is invalid in this context"- Right len -> return len- where- lengthOctetToLength :: Word8 -> Get (Either Integer Integer)- lengthOctetToLength f- | f < 192 = return . Right . fromIntegral $ f- | f < 224 = do- secondOctet <- getWord8- return . Right . fromIntegral $- shiftL (fromIntegral (f - 192) :: Int) 8 +- (fromIntegral secondOctet :: Int) +- 192- | f < 255 =- return . Left . fromIntegral $ (1 :: Integer) `shiftL` fromIntegral (f .&. 0x1f)- | otherwise = do- len <- getWord32be- return . Right . fromIntegral $ len--putPacketLength :: Integer -> Put-putPacketLength l- | l < 192 = putWord8 (fromIntegral l)- | l < 8384 =- putWord8 (fromIntegral ((fromIntegral (l - 192) `shiftR` 8) + 192 :: Int)) >>- putWord8 (fromIntegral (l - 192) .&. 0xff)- | l < 0x100000000 = putWord8 255 >> putWord32be (fromIntegral l)- | otherwise = error "packet length exceeds 32-bit definite length encoding"--putPartialLength :: Word8 -> Put-putPartialLength n = putWord8 (224 + n)--getPacketLengthFromOctet :: Word8 -> Get (Either Int64 Int64)-getPacketLengthFromOctet f- | f < 192 = return . Right . fromIntegral $ f- | f < 224 = do- secondOctet <- getWord8- return . Right . fromIntegral $- shiftL (fromIntegral (f - 192) :: Int) 8 +- (fromIntegral secondOctet :: Int) +- 192- | f < 255 =- return . Left . fromIntegral $ (1 :: Integer) `shiftL` fromIntegral (f .&. 0x1f)- | otherwise = do- len <- getWord32be- return . Right . fromIntegral $ len--getS2K :: Get S2K-getS2K = getS2K' =<< getWord8- where- getS2K' :: Word8 -> Get S2K- getS2K' t- | t == 0 = do- ha <- getWord8- return $ Simple (toFVal ha)- | t == 1 = do- ha <- getWord8- salt <- getByteString 8- return $ Salted (toFVal ha) (Salt8 salt)- | t == 3 = do- ha <- getWord8- salt <- getByteString 8- count <- getWord8- return $- IteratedSalted (toFVal ha) (Salt8 salt) (decodeIterationCount count)- | t == 4 = do- salt <- getByteString 16- passes <- getWord8- parallelism <- getWord8- encodedM <- getWord8- return $ Argon2 (Salt16 salt) passes parallelism encodedM- | otherwise = do- bs <- getRemainingLazyByteString- return $ OtherS2K t bs--putS2K :: S2K -> Put-putS2K (Simple hashalgo) = error ("confused by simple" ++ show hashalgo)-putS2K (Salted hashalgo salt) =- error ("confused by salted" ++ show hashalgo ++ " by " ++ show salt)-putS2K (IteratedSalted ha salt count) = do- putWord8 3- put ha- putByteString (unSalt8 salt)- putWord8 $ encodeIterationCount count-putS2K (Argon2 salt t p encodedM) = do- putWord8 4- putByteString (unSalt16 salt)- putWord8 t- putWord8 p- putWord8 encodedM-putS2K (OtherS2K t bs) = putWord8 t >> putLazyByteString bs--v6SaltSizeForHashAlgorithm :: HashAlgorithm -> Maybe Word8-v6SaltSizeForHashAlgorithm = signatureV6SaltSizeForHashAlgorithm--getPacketTypeAndPayload :: Get (Word8, ByteString)-getPacketTypeAndPayload = do- tag <- getWord8- guard (testBit tag 7)- case tag .&. 0x40 of- 0x00 -> do- let t = shiftR (tag .&. 0x3c) 2- case tag .&. 0x03 of- 0 -> do- len <- getWord8- bs <- getLazyByteString (fromIntegral len)- return (t, bs)- 1 -> do- len <- getWord16be- bs <- getLazyByteString (fromIntegral len)- return (t, bs)- 2 -> do- len <- getWord32be- bs <- getLazyByteString (fromIntegral len)- return (t, bs)- 3 -> do- bs <- getRemainingLazyByteString- return (t, bs)- _ -> error "This should never happen (getPacketTypeAndPayload/0x00)."- 0x40 -> do- firstLenOctet <- getWord8- bs <- getPacketPayloadFromLengthOctet firstLenOctet- return (tag .&. 0x3f, bs)- _ -> error "This should never happen (getPacketTypeAndPayload/???)."- where- getPacketPayloadFromLengthOctet :: Word8 -> Get ByteString- getPacketPayloadFromLengthOctet lenOctet = do- lenOrPartial <- getPacketLengthFromOctet lenOctet- case lenOrPartial of- Right len -> getLazyByteString len- Left partialLen -> do- chunk <- getLazyByteString partialLen- rest <- getRemainingPartialPayload- return (chunk <> rest)- getRemainingPartialPayload :: Get ByteString- getRemainingPartialPayload = do- lenOctet <- getWord8- lenOrPartial <- getPacketLengthFromOctet lenOctet- case lenOrPartial of- Right len -> getLazyByteString len- Left partialLen -> do- chunk <- getLazyByteString partialLen- (chunk <>) <$> getRemainingPartialPayload--getPkt :: Get Pkt-getPkt = do- (t, pl) <- getPacketTypeAndPayload- case runGetOrFail (getPkt' t (BL.length pl)) pl of- Left (_, _, e) -> return $! BrokenPacketPkt e t pl- Right (_, _, p) -> return p- where- parseLegacyPKESK :: PacketVersion -> BL.ByteString -> Either String Pkt- parseLegacyPKESK pv body = do- (_, _, (eokeyid, pkaRaw, mpib)) <-- bimap (\(_, _, e) -> e) id $- runGetOrFail- (do eokeyid <- getLazyByteString 8- pka <- getWord8- mpib <- getRemainingLazyByteString- pure (eokeyid, pka, mpib))- body- let pka = toFVal pkaRaw- sk <- parseLegacyPKESKMPIs pka mpib- pure $- PKESKPkt- (PKESKPayloadV3Packet (PKESKPayloadV3 pv (EightOctetKeyId eokeyid) pka sk))-- parseLegacyPKESKMPIs :: PubKeyAlgorithm -> BL.ByteString -> Either String (NE.NonEmpty MPI)- parseLegacyPKESKMPIs pka mpib = do- case parseLegacyPKESKMPIsStrict pka mpib of- Right sk -> pure sk- Left strictErr- | pka == X25519 ->- case parseLegacyPKESKX25519V3Octets mpib of- Right sk -> Right sk- Left octetErr ->- Left- (strictErr ++- "; also failed to parse RFC9580 X25519 v3 octet layout: " ++- octetErr)- | pka == ECDH ->- case parseLegacyPKESKECDHOctets mpib of- Right sk -> Right sk- Left octetErr ->- Left- (strictErr ++- "; also failed to parse RFC6637 ECDH v3 octet layout: " ++- octetErr)- | otherwise -> Left strictErr-- parseLegacyPKESKMPIsStrict ::- PubKeyAlgorithm -> BL.ByteString -> Either String (NE.NonEmpty MPI)- parseLegacyPKESKMPIsStrict pka mpib = do- (rest, _, sk) <-- bimap (\(_, _, e) -> e) id $- runGetOrFail (parserForLegacyPKESKMPIs pka) mpib- if BL.null rest- then pure (NE.fromList sk)- else- Left- ("unexpected trailing PKESK MPI data for algorithm " ++ show pka)-- parseLegacyPKESKX25519V3Octets :: BL.ByteString -> Either String (NE.NonEmpty MPI)- parseLegacyPKESKX25519V3Octets mpib = do- if BL.length mpib < 33- then Left "X25519 v3 PKESK octet layout is too short"- else Right ()- let ephemeral = BL.toStrict (BL.take 32 mpib)- eskLen = fromIntegral (BL.index mpib 32) :: Int- eskWithAlgo = BL.toStrict (BL.drop 33 mpib)- if eskLen /= B.length eskWithAlgo- then Left "X25519 v3 PKESK octet layout has inconsistent ESK length"- else Right ()- if B.null eskWithAlgo- then Left "X25519 v3 PKESK octet layout must include a symmetric algorithm octet"- else Right ()- let symAlgo = B.head eskWithAlgo- if symAlgo `elem` [fromIntegral (fromFVal AES128), fromIntegral (fromFVal AES192), fromIntegral (fromFVal AES256)]- then pure (NE.fromList [MPI (os2ip ephemeral), MPI (os2ip eskWithAlgo)])- else- Left- ("X25519 v3 PKESK octet layout has unsupported symmetric algorithm octet " ++- show symAlgo)-- -- | Parse an RFC 6637 §8 ECDH PKESKv3 body as MPI(ephemeral) || 1-octet-count || C.- -- This is the interoperable wire format produced by GnuPG and other RFC-compliant- -- implementations. hOpenPGP previously wrote both fields as MPIs; this fallback- -- allows reading RFC-compliant packets when the strict two-MPI path fails.- parseLegacyPKESKECDHOctets :: BL.ByteString -> Either String (NE.NonEmpty MPI)- parseLegacyPKESKECDHOctets mpib = do- (rest, _, ephMPI) <-- bimap (\(_, _, e) -> e) id $ runGetOrFail getMPI mpib- let restBS = BL.toStrict rest- when (B.null restBS) $- Left "ECDH v3 PKESK RFC6637 octet layout: missing wrapped-key length octet after ephemeral MPI"- let wrappedLen = fromIntegral (B.head restBS) :: Int- wrapped = B.tail restBS- when (wrappedLen /= B.length wrapped) $- Left- ("ECDH v3 PKESK RFC6637 octet layout: wrapped key length field " ++- show wrappedLen ++ " does not match body length " ++ show (B.length wrapped))- when (wrappedLen < 24 || wrappedLen `mod` 8 /= 0) $- Left- ("ECDH v3 PKESK RFC6637 octet layout: wrapped key length " ++- show wrappedLen ++ " is not a valid RFC 3394 wrapped key size")- pure (ephMPI NE.:| [MPI (os2ip wrapped)])-- parserForLegacyPKESKMPIs :: PubKeyAlgorithm -> Get [MPI]- parserForLegacyPKESKMPIs pka =- case expectedLegacyPKESKMPIArity pka of- Just mpiCount -> replicateM mpiCount getMPI- Nothing -> some getMPI-- expectedLegacyPKESKMPIArity :: PubKeyAlgorithm -> Maybe Int- expectedLegacyPKESKMPIArity pka- | pka `elem` [RSA, DeprecatedRSAEncryptOnly] = Just 1- | pka `elem` [ElgamalEncryptOnly, ForbiddenElgamal, ECDH, X25519] = Just 2- | otherwise = Nothing-- validateV4SKESKEncryptedSessionKeyS2K :: S2K -> Maybe BL.ByteString -> Get ()- validateV4SKESKEncryptedSessionKeyS2K _ Nothing = pure ()- validateV4SKESKEncryptedSessionKeyS2K Simple {} (Just _) =- fail- "v4 SKESK packets with encrypted session keys must not use Simple S2K"- validateV4SKESKEncryptedSessionKeyS2K _ (Just _) = pure ()-- parseV6PKESK :: BL.ByteString -> Either String Pkt- parseV6PKESK body = do- (_, _, (recipientKeyIdentifier, pka, esk)) <-- bimap (\(_, _, e) -> e) id $- runGetOrFail- (do keyIdentifierLen <- getWord8- recipientKeyIdentifier <- getLazyByteString (fromIntegral keyIdentifierLen)- pka <- getWord8- esk <- getRemainingLazyByteString- pure (recipientKeyIdentifier, pka, esk))- body- validateV6PKESKRecipientIdentifier recipientKeyIdentifier- pure $- PKESKPkt- (PKESKPayloadV6Packet (PKESKPayloadV6 recipientKeyIdentifier (toFVal pka) esk))- where- validateV6PKESKRecipientIdentifier :: BL.ByteString -> Either String ()- validateV6PKESKRecipientIdentifier rid =- case BL.length rid of- 0 -> Right ()- 20 -> Right ()- 32 -> Right ()- 21 -> validateVersionedFingerprint rid- 33 -> validateVersionedFingerprint rid- ridLen ->- Left- ("invalid PKESK v6 recipient identifier length: " ++- show ridLen ++- " (expected 0, 20, 21, 32, or 33)")-- validateVersionedFingerprint :: BL.ByteString -> Either String ()- validateVersionedFingerprint rid =- let keyVersion = BL.head rid- fingerprintLen = BL.length (BL.tail rid)- in case keyVersion of- 4 ->- if fingerprintLen == 20- then Right ()- else- Left- ("PKESK v6 recipient identifier length/version mismatch: key version 4 requires fingerprint length 20, got " ++- show fingerprintLen)- 6 ->- if fingerprintLen == 32- then Right ()- else- Left- ("PKESK v6 recipient identifier length/version mismatch: key version 6 requires fingerprint length 32, got " ++- show fingerprintLen)- _ ->- Left- ("invalid PKESK v6 recipient key version: " ++- show keyVersion ++ " (expected 4 or 6)")-- getPkt' :: Word8 -> ByteOffset -> Get Pkt- getPkt' t len- | t == 1 = do- pv <- getWord8- body <- getRemainingLazyByteString- if pv == 6- then case parseV6PKESK body of- Right pkt -> return pkt- Left v6Err -> fail ("PKESK v6 parse failed: " ++ v6Err)- else case parseLegacyPKESK pv body of- Right pkt -> return pkt- Left legacyErr -> fail ("PKESK MPIs " ++ legacyErr)- | t == 2 = do- bs <- getRemainingLazyByteString- case runGetOrFail get bs of- Left (_, _, e) -> fail ("signature packet " ++ e)- Right (_, _, sp) -> return $ SignaturePkt sp- | t == 3 = do- pv <- getWord8- let getV6SKESKParams = do- symalgoWord <- getWord8- aeadWord <- getWord8- s2kLen <- getWord8- s2kBytes <- getLazyByteString (fromIntegral s2kLen)- s2k <-- case runGetOrFail getS2K s2kBytes of- Left (_, _, err) -> fail err- Right (rest, _, parsed)- | not (BL.null rest) -> fail "unexpected trailing bytes in v6 SKESK S2K specifier"- | otherwise -> pure parsed- let symalgo = toFVal symalgoWord- aead = toFVal aeadWord- ivLen = fromIntegral (aeadNonceSize aead)- iv <- getLazyByteString ivLen- pure (symalgo, aead, s2k, iv)- case pv of- 6 -> do- paramsLen <- getWord8- params <- getLazyByteString (fromIntegral paramsLen)- (symalgo, aead, s2k, iv) <-- case runGetOrFail getV6SKESKParams params of- Left (_, _, err) -> fail err- Right (rest, _, parsed)- | not (BL.null rest) -> fail "unexpected trailing v6 SKESK parameters"- | otherwise -> pure parsed- payload <- getRemainingLazyByteString- when (BL.length payload < 16) $- fail "v6 SKESK payload must include encrypted session key and authentication tag"- let (esk, tag) = BL.splitAt (BL.length payload - 16) payload- return $- SKESKPkt- (SKESKPayloadV6Packet- (SKESKPayloadV6- symalgo- aead- s2k- iv- esk- tag))- 4 -> do- symalgo <- getWord8- s2k <- getS2K- esk <- getRemainingLazyByteString- let mesk = if BL.null esk then Nothing else Just esk- validateV4SKESKEncryptedSessionKeyS2K s2k mesk- return $- SKESKPkt- (SKESKPayloadV4Packet- (SKESKPayloadV4- (toFVal symalgo)- s2k- mesk))- _ -> fail ("unsupported SKESK packet version " ++ show pv)- | t == 4 = do- pv <- getWord8- sigtype <- toFVal <$> getWord8- ha <- toFVal <$> getWord8- pka <- toFVal <$> getWord8- case pv of- 3 -> do- skeyid <- getLazyByteString 8- nested <- getWord8 >>= parseOPSNestedFlag- return $- OnePassSignaturePkt- (OPSPayloadV3Packet- (OPSPayloadV3- pv- sigtype- ha- pka- (EightOctetKeyId skeyid)- nested))- 6 -> do- saltSize <- getWord8- expectedSaltSize <-- maybe- (fail ("signature hash algorithm does not define a V6 salt size: " ++ show ha))- pure- (v6SaltSizeForHashAlgorithm ha)- when (saltSize /= expectedSaltSize) $- fail- ("OPS v6 salt size mismatch for " ++- show ha ++ ": expected " ++ show expectedSaltSize ++ ", got " ++ show saltSize)- salt <- SignatureSalt <$> getLazyByteString (fromIntegral saltSize)- signerFingerprint <- getLazyByteString 32- nested <- getWord8 >>= parseOPSNestedFlag- return $- OnePassSignaturePkt- (OPSPayloadV6Packet- (OPSPayloadV6- sigtype- ha- pka- salt- signerFingerprint- nested))- _ -> fail ("Unsupported OPS version: " ++ show pv)- | t == 5 = do- bs <- getLazyByteString len- let ps =- flip runGetOrFail bs $ do- pkp <- getPKPayload- ska <- getSKAddendum pkp- return $ SecretKeyPkt pkp ska- case ps of- Left (_, _, err) -> fail ("secret key " ++ err)- Right (_, _, pkt) -> return pkt- | t == 6 = do- pkp <- getPKPayload- return $ PublicKeyPkt pkp- | t == 7 = do- bs <- getLazyByteString len- let ps =- flip runGetOrFail bs $ do- pkp <- getPKPayload- ska <- getSKAddendum pkp- return $ SecretSubkeyPkt pkp ska- case ps of- Left (_, _, err) -> fail ("secret subkey " ++ err)- Right (_, _, pkt) -> return pkt- | t == 8 = do- ca <- getWord8- cdata <- getLazyByteString (len - 1)- return $ CompressedDataPkt (toFVal ca) cdata- | t == 9 = do- sdata <- getLazyByteString len- return $ SymEncDataPkt sdata- | t == 10 = do- marker <- getLazyByteString len- return $ MarkerPkt marker- | t == 11 = do- dt <- getWord8- flen <- getWord8- fn <- getLazyByteString (fromIntegral flen)- ts <- fmap ThirtyTwoBitTimeStamp getWord32be- ldata <- getLazyByteString (len - (6 + fromIntegral flen))- return $ LiteralDataPkt (toFVal dt) fn ts ldata- | t == 12 = do- tdata <- getLazyByteString len- return $ TrustPkt tdata- | t == 13 = do- udata <- getByteString (fromIntegral len)- return . UserIdPkt . decodeUtf8With lenientDecode $ udata- | t == 14 = do- bs <- getLazyByteString len- let ps =- flip runGetOrFail bs $ do- pkp <- getPKPayload- return $ PublicSubkeyPkt pkp- case ps of- Left (_, _, err) -> fail ("public subkey " ++ err)- Right (_, _, pkt) -> return pkt- | t == 17 = do- bs <- getLazyByteString len- case runGetOrFail (many getUserAttrSubPacket) bs of- Left (_, _, err) -> fail ("user attribute " ++ err)- Right (_, _, uas) -> return $ UserAttributePkt uas- | t == 18 = do- pv <- getWord8- case pv of- 1 -> do- b <- getLazyByteString (len - 1)- return $ SymEncIntegrityProtectedDataPkt (SEIPD1 pv b)- 2 -> do- when (len < 36) $- fail "SEIPD v2 packet too short"- symalgo <- toFVal <$> getWord8- aeadalgo <- toFVal <$> getWord8- chunkSize <- getWord8- salt <- Salt <$> getByteString 32- encrypted <- getLazyByteString (len - 36)- validateSEIPDv2Header symalgo aeadalgo chunkSize encrypted- return $- SymEncIntegrityProtectedDataPkt- (SEIPD2- symalgo- aeadalgo- chunkSize- salt- encrypted)- _ -> fail ("Unsupported SEIPD version: " ++ show pv)- | t == 19 = do- hash <- getLazyByteString 20- return $ ModificationDetectionCodePkt hash- | otherwise = do- payload <- getLazyByteString len- return $ OtherPacketPkt t payload--getUserAttrSubPacket :: Get UserAttrSubPacket-getUserAttrSubPacket = do- l <- fmap fromIntegral getSubPacketLength- t <- getWord8- getUserAttrSubPacket' t l- where- getUserAttrSubPacket' :: Word8 -> ByteOffset -> Get UserAttrSubPacket- getUserAttrSubPacket' t l- | t == 1 = do- _ <- getWord16le -- ihlen- hver <- getWord8 -- should be 1- iformat <- getWord8- nuls <- getLazyByteString 12 -- should be NULs- bs <- getLazyByteString (l - 17)- if hver /= 1 || nuls /= BL.pack (replicate 12 0)- then fail "Corrupt UAt subpacket"- else return $ ImageAttribute (ImageHV1 (toFVal iformat)) bs- | otherwise = do- bs <- getLazyByteString (l - 1)- return $ OtherUASub t bs--putUserAttrSubPacket :: UserAttrSubPacket -> Put-putUserAttrSubPacket ua = do- let sp = runPut $ putUserAttrSubPacket' ua- putSubPacketLength . fromIntegral . BL.length $ sp- putLazyByteString sp- where- putUserAttrSubPacket' (ImageAttribute (ImageHV1 iformat) idata) = do- putWord8 1- putWord16le 16- putWord8 1- putWord8 (fromFVal iformat)- replicateM_ 12 $ putWord8 0- putLazyByteString idata- putUserAttrSubPacket' (OtherUASub t bs) = do- putWord8 t- putLazyByteString bs---- | Serialize PKESKv3 session-key material.--- For ECDH and X25519 the RFC 6637 §8 / RFC 9580 §5.1.6 wire format is used:--- MPI(ephemeral_key) || 1-octet-count || wrapped_session_key_bytes.--- All other algorithms use the standard MPI sequence.-putPKESKv3SessionKeyMaterial :: PubKeyAlgorithm -> NE.NonEmpty MPI -> Put-putPKESKv3SessionKeyMaterial pka mpis- | pka `elem` [ECDH, X25519]- , (ephMPI NE.:| [wrappedMPI]) <- mpis = do- put ephMPI- let rawWrapped = i2osp (unMPI wrappedMPI)- -- Left-pad to the nearest valid RFC 3394 wrapped-key length so that- -- leading-zero bytes stripped by i2osp are restored.- targetLen = headDef (B.length rawWrapped) (filter (>= B.length rawWrapped) [32, 40, 48])- paddedWrapped = leftPadTo targetLen rawWrapped- putWord8 (fromIntegral (B.length paddedWrapped))- putByteString paddedWrapped- | otherwise = F.mapM_ put mpis- where- headDef d [] = d- headDef _ (x:_) = x--putPkt :: Pkt -> Put-putPkt (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 pv eokeyid pka mpis))) = do- putWord8 (0xc0 .|. 1)- let bsk = runPut $ putPKESKv3SessionKeyMaterial pka mpis- putPacketLength . fromIntegral $ 10 + BL.length bsk- putWord8 pv -- must be 3- putLazyByteString (unEOKI eokeyid) -- must be 8 octets- putWord8 $ fromIntegral . fromFVal $ pka- putLazyByteString bsk-putPkt (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 recipientKeyIdentifier pka esk))) = do- putWord8 (0xc0 .|. 1)- let keyIdentifierLen = BL.length recipientKeyIdentifier- when (keyIdentifierLen > 255) $- error "PKESK v6 recipient key identifier must fit in one octet"- putPacketLength . fromIntegral $ 3 + keyIdentifierLen + BL.length esk- putWord8 6- putWord8 (fromIntegral keyIdentifierLen)- putLazyByteString recipientKeyIdentifier- putWord8 $ fromIntegral . fromFVal $ pka- putLazyByteString esk-putPkt (SignaturePkt sp) = do- putWord8 (0xc0 .|. 2)- let bs = runPut $ put sp- putLengthThenPayload bs-putPkt (SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 symalgo s2k mesk))) = do- putWord8 (0xc0 .|. 3)- let bs2k = fromS2K s2k- let bsk = fromMaybe BL.empty mesk- putPacketLength . fromIntegral $ 2 + BL.length bs2k + BL.length bsk- putWord8 4- putWord8 $ fromIntegral . fromFVal $ symalgo- putLazyByteString bs2k- putLazyByteString bsk-putPkt (SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 symalgo aead s2k iv esk tag))) = do- putWord8 (0xc0 .|. 3)- let bs2k = fromS2K s2k- let params =- BL.pack- [ fromIntegral (fromFVal symalgo)- , fromIntegral (fromFVal aead)- , fromIntegral (BL.length bs2k)- ] <>- bs2k <> iv- putPacketLength . fromIntegral $ 2 + BL.length params + BL.length esk + BL.length tag- putWord8 6- putWord8 (fromIntegral (BL.length params))- putLazyByteString params- putLazyByteString esk- putLazyByteString tag-putPkt (OnePassSignaturePkt (OPSPayloadV3Packet (OPSPayloadV3 pv sigtype ha pka skeyid nested))) = do- putWord8 (0xc0 .|. 4)- let bs =- runPut $ do- putWord8 pv -- should be 3- putWord8 $ fromIntegral . fromFVal $ sigtype- putWord8 $ fromIntegral . fromFVal $ ha- putWord8 $ fromIntegral . fromFVal $ pka- putLazyByteString (unEOKI skeyid)- putWord8 . fromIntegral . fromEnum $ not nested- putLengthThenPayload bs-putPkt (OnePassSignaturePkt (OPSPayloadV6Packet (OPSPayloadV6 sigtype ha pka salt signerFingerprint nested))) = do- putWord8 (0xc0 .|. 4)- let saltBytes = unSignatureSalt salt- saltSize = BL.length saltBytes- expectedSaltSize =- maybe- (error ("signature hash algorithm does not define a V6 salt size: " ++ show ha))- id- (v6SaltSizeForHashAlgorithm ha)- when (fromIntegral saltSize /= expectedSaltSize) $- error- ("OPS v6 salt size mismatch for " ++- show ha ++ ": expected " ++ show expectedSaltSize ++ ", got " ++ show saltSize)- when (BL.length signerFingerprint /= 32) $- error "OPS v6 signer fingerprint must be exactly 32 octets"- let bs =- runPut $ do- putWord8 6- putWord8 $ fromIntegral . fromFVal $ sigtype- putWord8 $ fromIntegral . fromFVal $ ha- putWord8 $ fromIntegral . fromFVal $ pka- putWord8 (fromIntegral saltSize)- putLazyByteString saltBytes- putLazyByteString signerFingerprint- putWord8 . fromIntegral . fromEnum $ not nested- putLengthThenPayload bs-putPkt (SecretKeyPkt pkp ska) = do- putWord8 (0xc0 .|. 5)- let bs = runPut (putPKPayload pkp >> putSKAddendumForPKPayload pkp ska)- putLengthThenPayload bs-putPkt (PublicKeyPkt pkp) = do- putWord8 (0xc0 .|. 6)- let bs = runPut $ putPKPayload pkp- putLengthThenPayload bs-putPkt (SecretSubkeyPkt pkp ska) = do- putWord8 (0xc0 .|. 7)- let bs = runPut (putPKPayload pkp >> putSKAddendumForPKPayload pkp ska)- putLengthThenPayload bs-putPkt (CompressedDataPkt ca cdata) = do- putWord8 (0xc0 .|. 8)- let bs =- runPut $ do- putWord8 $ fromIntegral . fromFVal $ ca- putLazyByteString cdata- putLengthThenPayload bs-putPkt (SymEncDataPkt b) = do- putWord8 (0xc0 .|. 9)- putLengthThenPayload b-putPkt (MarkerPkt b) = do- putWord8 (0xc0 .|. 10)- putLengthThenPayload b-putPkt (LiteralDataPkt dt fn ts b) = do- putWord8 (0xc0 .|. 11)- let bs =- runPut $ do- putWord8 $ fromIntegral . fromFVal $ dt- putWord8 $ fromIntegral . BL.length $ fn- putLazyByteString fn- putWord32be . unThirtyTwoBitTimeStamp $ ts- putLazyByteString b- putLengthThenPayload bs-putPkt (TrustPkt b) = do- putWord8 (0xc0 .|. 12)- putLengthThenPayload b-putPkt (UserIdPkt u) = do- putWord8 (0xc0 .|. 13)- let bs = encodeUtf8 u- putPacketLength . fromIntegral $ B.length bs- putByteString bs-putPkt (PublicSubkeyPkt pkp) = do- putWord8 (0xc0 .|. 14)- let bs = runPut $ putPKPayload pkp- putLengthThenPayload bs-putPkt (UserAttributePkt us) = do- putWord8 (0xc0 .|. 17)- let bs = runPut $ mapM_ put us- putLengthThenPayload bs-putPkt (SymEncIntegrityProtectedDataPkt (SEIPD1 pv b)) = do- putWord8 (0xc0 .|. 18)- putPacketLength . fromIntegral $ BL.length b + 1- putWord8 pv -- should be 1- putLazyByteString b-putPkt (SymEncIntegrityProtectedDataPkt (SEIPD2 symalgo aeadalgo chunkSize salt b)) = do- when (B.length (unSalt salt) /= 32) $- error "SEIPD v2 salt must be exactly 32 octets"- when (chunkSize > 16) $- error "SEIPD v2 chunk size octet must be between 0 and 16"- case symalgo of- OtherSA _ -> error "SEIPD v2 requires a known symmetric algorithm"- Plaintext -> error "SEIPD v2 cannot use plaintext cipher"- _ -> return ()- case aeadalgo of- OtherAEADAlgo _ -> error "SEIPD v2 requires a known AEAD algorithm"- _ -> return ()- putWord8 (0xc0 .|. 18)- putPacketLength . fromIntegral $ BL.length b + 36- putWord8 2- putWord8 (fromFVal symalgo)- putWord8 (fromFVal aeadalgo)- putWord8 chunkSize- putByteString (unSalt salt)- putLazyByteString b-putPkt (ModificationDetectionCodePkt hash) = do- putWord8 (0xc0 .|. 19)- putLengthThenPayload hash-putPkt (OtherPacketPkt t payload) = do- when (t > 63) $- error ("cannot serialize OtherPacket packet tag > 63: " ++ show t)- putWord8 (0xc0 .|. t)- putLengthThenPayload payload-putPkt (BrokenPacketPkt _ t payload) = putPkt (OtherPacketPkt t payload)---- | Validate a packet before serialization to catch constraint violations early.--- Returns Left with descriptive error if validation fails.-validatePkt :: Pkt -> Either String ()-validatePkt (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 recipientKeyIdentifier _ _))) = do- let keyIdentifierLen = BL.length recipientKeyIdentifier- when (keyIdentifierLen > 255) $- Left "PKESK v6 recipient key identifier must fit in one octet (max 255 bytes)"- Right ()-validatePkt (OnePassSignaturePkt (OPSPayloadV6Packet (OPSPayloadV6 _ ha _ salt signerFingerprint _))) = do- let saltBytes = unSignatureSalt salt- saltSize = BL.length saltBytes- expectedSaltSize <-- case v6SaltSizeForHashAlgorithm ha of- Nothing -> Left $ "signature hash algorithm does not define a V6 salt size: " ++ show ha- Just sz -> Right sz- when (fromIntegral saltSize /= expectedSaltSize) $- Left- ("OPS v6 salt size mismatch for " ++- show ha ++ ": expected " ++ show expectedSaltSize ++ ", got " ++ show saltSize)- when (BL.length signerFingerprint /= 32) $- Left "OPS v6 signer fingerprint must be exactly 32 octets"- Right ()-validatePkt (SymEncIntegrityProtectedDataPkt (SEIPD2 symalgo aeadalgo chunkSize salt _)) = do- when (B.length (unSalt salt) /= 32) $- Left "SEIPD v2 salt must be exactly 32 octets"- when (chunkSize > 16) $- Left "SEIPD v2 chunk size octet must be between 0 and 16"- case symalgo of- OtherSA _ -> Left "SEIPD v2 requires a known symmetric algorithm"- Plaintext -> Left "SEIPD v2 cannot use plaintext cipher"- _ -> Right ()- case aeadalgo of- OtherAEADAlgo _ -> Left "SEIPD v2 requires a known AEAD algorithm"- _ -> Right ()-validatePkt (OtherPacketPkt t _) = do- when (t > 63) $- Left ("cannot serialize OtherPacket packet tag > 63: " ++ show t)- Right ()-validatePkt _ = Right ()---- | Serialize a packet with explicit validation and error handling.--- Validates constraints before calling putPkt to ensure errors are caught early.-putPktEither :: Pkt -> Either String Put-putPktEither pkt = case validatePkt pkt of- Left err -> Left err- Right () -> Right (putPkt pkt)--putLengthThenPayload :: ByteString -> Put-putLengthThenPayload bs = do- let len = BL.length bs- if len < fromIntegral (0x100000000 :: Integer)- then do- putPacketLength (fromIntegral len)- putLazyByteString bs- else putPartialLengthPayload bs- where- maxPartialChunkSize :: Int64- maxPartialChunkSize = 1 `shiftL` (30 :: Int)- putPartialLengthPayload :: ByteString -> Put- putPartialLengthPayload payload- | BL.length payload > maxPartialChunkSize = do- let (chunk, rest) = BL.splitAt maxPartialChunkSize payload- putPartialLength 30- putLazyByteString chunk- putPartialLengthPayload rest- | otherwise = do- putPacketLength (fromIntegral (BL.length payload))- putLazyByteString payload--validateSEIPDv2Header ::- SymmetricAlgorithm -> AEADAlgorithm -> Word8 -> ByteString -> Get ()-validateSEIPDv2Header symalgo aeadalgo chunkSize encrypted = do- when (chunkSize > 16) $- fail "SEIPD v2 chunk size octet must be between 0 and 16"- when (BL.null encrypted) $- fail "SEIPD v2 payload is missing encrypted data and final authentication tag"- case symalgo of- OtherSA _ -> fail "SEIPD v2 requires a known symmetric algorithm"- Plaintext -> fail "SEIPD v2 cannot use plaintext cipher"- _ -> return ()- case aeadalgo of- OtherAEADAlgo _ -> fail "SEIPD v2 requires a known AEAD algorithm"- _ -> return ()--getMPI :: Get MPI-getMPI = do- mpilen <- getWord16be- bs <- getByteString (fromIntegral (mpilen + 7) `div` 8)- return $ MPI (os2ip bs)--getPubkey :: PubKeyAlgorithm -> Get PKey-getPubkey RSA = do- MPI n <- get- MPI e <- get- return $- RSAPubKey- (RSA_PublicKey (R.PublicKey (fromIntegral . B.length . i2osp $ n) n e))-getPubkey DeprecatedRSAEncryptOnly = getPubkey RSA-getPubkey DeprecatedRSASignOnly = getPubkey RSA-getPubkey DSA = do- MPI p <- get- MPI q <- get- MPI g <- get- MPI y <- get- return $ DSAPubKey (DSA_PublicKey (D.PublicKey (D.Params p g q) y))-getPubkey ElgamalEncryptOnly = getPubkey ForbiddenElgamal-getPubkey ForbiddenElgamal = do- MPI p <- get- MPI g <- get- MPI y <- get- return $ ElGamalPubKey p g y-getPubkey ECDSA = do- curvelength <- getWord8- when (curvelength == 0 || curvelength == 0xff) $- fail "invalid ECC curve OID length octet (reserved value)"- curveoid <- getByteString (fromIntegral curvelength)- MPI mpi <- getMPI- case curveoidBSToCurve curveoid of- Left e -> fail e- Right Curve25519 ->- EdDSAPubKey P.Ed25519 <$> (PrefixedNativeEPoint <$> validatePrefixedNativePoint 32 "Curve25519Legacy" mpi)- Right curve ->- case bs2Point (i2osp mpi) of- Left e -> fail e- Right point ->- return . ECDSAPubKey . ECDSA_PublicKey .- ECDSA.PublicKey (curve2Curve curve) $- point-getPubkey ECDH = do- ed <- getPubkey ECDSA -- could be an ECDSA or an EdDSA- kdflen <- getWord8- when (kdflen == 0 || kdflen == 0xff) $- fail "invalid ECDH KDF field length octet (reserved value)"- when (kdflen /= 3) $- fail ("invalid ECDH KDF field length: " ++ show kdflen)- one <- getWord8- when (one /= 1) $- fail ("invalid ECDH KDF reserved octet: " ++ show one)- kdfHA <- get- kdfSA <- get- return $ ECDHPubKey ed kdfHA kdfSA-getPubkey EdDSA = do- curvelength <- getWord8- when (curvelength == 0 || curvelength == 0xff) $- fail "invalid EdDSA curve OID length octet (reserved value)"- curveoid <- getByteString (fromIntegral curvelength)- MPI mpi <- getMPI- case curveoidBSToEdSigningCurve curveoid of- Left e -> fail e- Right P.Ed25519 ->- EdDSAPubKey P.Ed25519 <$> (PrefixedNativeEPoint <$> validatePrefixedNativePoint 32 "Ed25519Legacy" mpi)- Right P.Ed448 ->- EdDSAPubKey P.Ed448 <$> (PrefixedNativeEPoint <$> validatePrefixedNativePoint 57 "Ed448Legacy" mpi)-getPubkey pka | pka == BTypes.Ed25519 =- parseFixedLengthOrLegacyPubkey- 32- (EdDSAPubKey P.Ed25519 . NativeEPoint . EPoint . os2ip . BL.toStrict)- (getPubkey EdDSA)-getPubkey pka | pka == BTypes.Ed448 =- parseFixedLengthOrLegacyPubkey- 57- (EdDSAPubKey P.Ed448 . NativeEPoint . EPoint . os2ip . BL.toStrict)- (getPubkey EdDSA)-getPubkey X25519 =- parseFixedLengthOrLegacyPubkey- 32- (EdDSAPubKey P.Ed25519 . NativeEPoint . EPoint . os2ip . BL.toStrict)- (getPubkey ECDH)-getPubkey X448 =- parseFixedLengthOrLegacyPubkey- 56- (EdDSAPubKey P.Ed448 . NativeEPoint . EPoint . os2ip . BL.toStrict)- (getPubkey ECDH)-getPubkey _ = UnknownPKey <$> getRemainingLazyByteString--parseFixedLengthOrLegacyPubkey :: Int64 -> (BL.ByteString -> PKey) -> Get PKey -> Get PKey-parseFixedLengthOrLegacyPubkey expectedLen decodeFixed legacyParser = do- remaining <- lookAhead getRemainingLazyByteString- if BL.length remaining == expectedLen- then decodeFixed <$> getLazyByteString expectedLen- else legacyParser--getPubkeyV6 :: PubKeyAlgorithm -> Get PKey-getPubkeyV6 pka- | pka == BTypes.Ed25519 = do- len <- getWord32be- bs <- getByteString (fromIntegral len)- when (B.length bs /= 32) $- fail "invalid v6 Ed25519 public key length"- return $ EdDSAPubKey P.Ed25519 (NativeEPoint (EPoint (os2ip bs)))- | pka == BTypes.Ed448 = do- len <- getWord32be- bs <- getByteString (fromIntegral len)- when (B.length bs /= 57) $- fail "invalid v6 Ed448 public key length"- return $ EdDSAPubKey P.Ed448 (NativeEPoint (EPoint (os2ip bs)))- | pka == BTypes.X25519 = do- len <- getWord32be- bs <- getByteString (fromIntegral len)- when (B.length bs /= 32) $- fail "invalid v6 X25519 public key length"- return $ EdDSAPubKey P.Ed25519 (NativeEPoint (EPoint (os2ip bs)))- | pka == BTypes.X448 = do- len <- getWord32be- bs <- getByteString (fromIntegral len)- when (B.length bs /= 56) $- fail "invalid v6 X448 public key length"- return $ EdDSAPubKey P.Ed448 (NativeEPoint (EPoint (os2ip bs)))- | otherwise = getPubkey pka--bs2Point :: B.ByteString -> Either String ECDSA.PublicPoint-bs2Point bs =- if B.null bs- then Left "empty EC point encoding"- else- let xy = B.drop 1 bs- l = B.length xy- in if B.head bs /= 0x04- then Left $ "unknown type of point: " ++ show (B.unpack bs)- else if odd l- then Left "malformed EC point encoding: odd coordinate payload length"- else- return- (uncurry- ECCT.Point- ((os2ip *** os2ip) (B.splitAt (div l 2) xy)))--putPubkey :: PKey -> Put-putPubkey (UnknownPKey bs) = putLazyByteString bs-putPubkey p@(ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _))) =- let Right curveoidbs = curveToCurveoidBS (curveFromCurve curve)- in putCurveOID curveoidbs >>- mapM_ put (pubkeyToMPIs p)-putPubkey p@(ECDHPubKey (ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _))) kha ksa) =- let Right curveoidbs = curveToCurveoidBS (curveFromCurve curve)- in putCurveOID curveoidbs >>- mapM_ put (pubkeyToMPIs p) >>- putECDHKDFParams kha ksa-putPubkey p@(ECDHPubKey (EdDSAPubKey curve (PrefixedNativeEPoint _)) kha ksa) =- let Right curveoidbs = curveToCurveoidBS (ed2ec curve)- in putCurveOID curveoidbs >>- mapM_ put (pubkeyToMPIs p) >>- putECDHKDFParams kha ksa- where- ed2ec P.Ed25519 = Curve25519- ed2ec P.Ed448 = Curve448-putPubkey p@(EdDSAPubKey curve (PrefixedNativeEPoint _)) =- let Right curveoidbs = edSigningCurveToCurveoidBS curve- in putCurveOID curveoidbs >>- mapM_ put (pubkeyToMPIs p)-putPubkey (ECDHPubKey (EdDSAPubKey curve (NativeEPoint _)) _ _) =- error ("legacy ECDH serialization requires a prefixed-native " ++ show curve ++ " point")-putPubkey (EdDSAPubKey curve (NativeEPoint _)) =- error ("legacy EdDSA serialization requires a prefixed-native " ++ show curve ++ " point")-putPubkey p = mapM_ put (pubkeyToMPIs p)--putPubkeyV6 :: PKey -> Put-putPubkeyV6 (EdDSAPubKey P.Ed25519 (NativeEPoint (EPoint x))) = do- let bs = fixedLengthOctets 32 x- putWord32be . fromIntegral . B.length $ bs- putByteString bs-putPubkeyV6 (EdDSAPubKey P.Ed448 (NativeEPoint (EPoint x))) = do- let bs = fixedLengthOctets 57 x- putWord32be . fromIntegral . B.length $ bs- putByteString bs-putPubkeyV6 (ECDHPubKey (EdDSAPubKey P.Ed25519 (NativeEPoint (EPoint x))) kha ksa) = do- let bs = fixedLengthOctets 32 x- putWord32be . fromIntegral . B.length $ bs- putByteString bs- put kha- put ksa-putPubkeyV6 (ECDHPubKey (EdDSAPubKey P.Ed448 (NativeEPoint (EPoint x))) kha ksa) = do- let bs = fixedLengthOctets 56 x- putWord32be . fromIntegral . B.length $ bs- putByteString bs- put kha- put ksa-putPubkeyV6 p = putPubkey p--fixedLengthOctets :: Int -> Integer -> B.ByteString-fixedLengthOctets targetLen x =- let bs = i2osp x- in if B.length bs > targetLen- then error ("public key element does not fit in " ++ show targetLen ++ " octets")- else B.replicate (targetLen - B.length bs) 0 <> bs--validatePrefixedNativePoint :: Int -> String -> Integer -> Get EPoint-validatePrefixedNativePoint targetLen label i =- let bs = i2osp i- in if B.length bs /= targetLen + 1- then- fail- ("invalid " ++ label ++ " public key length: expected " ++- show (targetLen + 1) ++ " octets with 0x40 prefix, got " ++ show (B.length bs))- else- if B.head bs /= 0x40- then fail ("invalid " ++ label ++ " public key: missing 0x40 prefix")- else pure (EPoint i)--putCurveOID :: B.ByteString -> Put-putCurveOID oid = do- let oidLength = B.length oid- when (oidLength == 0 || oidLength == 0xff) $- error "curve OID length cannot use reserved values 0 or 255"- putWord8 (fromIntegral oidLength)- putByteString oid--putECDHKDFParams :: HashAlgorithm -> SymmetricAlgorithm -> Put-putECDHKDFParams kdfHA kdfSA = do- let kdfLengthOctet = 0x03- when (kdfLengthOctet == 0 || kdfLengthOctet == 0xff) $- error "ECDH KDF field length cannot use reserved values 0 or 255"- putWord8 kdfLengthOctet- putWord8 0x01- put kdfHA- put kdfSA--parseOPSNestedFlag :: Word8 -> Get NestedFlag-parseOPSNestedFlag 0 = pure True-parseOPSNestedFlag 1 = pure False-parseOPSNestedFlag other =- fail ("invalid OPS nested flag octet: " ++ show other)--getSecretKey :: SomePKPayload -> Get SKey-getSecretKey pkp- | _pkalgo pkp `elem` [RSA, DeprecatedRSAEncryptOnly, DeprecatedRSASignOnly] = do- MPI d <- get- MPI p <- get- MPI q <- get- MPI _ <- get -- u- case inverse q p of- Nothing -> fail "invalid RSA secret key: q has no inverse modulo p"- Just qinv -> do- let dP = d `mod` (p - 1)- dQ = d `mod` (q - 1)- pub = (\(RSAPubKey (RSA_PublicKey x)) -> x) (_pubkey pkp)- return $ RSAPrivateKey (RSA_PrivateKey (R.PrivateKey pub d p q dP dQ qinv))- | _pkalgo pkp == DSA = do- MPI x <- get- return $ DSAPrivateKey (DSA_PrivateKey (D.PrivateKey (D.Params 0 0 0) x))- | _pkalgo pkp `elem` [ElgamalEncryptOnly, ForbiddenElgamal] = do- MPI x <- get- return $ ElGamalPrivateKey x- | _pkalgo pkp == ECDSA = do- let pubcurve =- (\(ECDSAPubKey (ECDSA_PublicKey p)) -> ECDSA.public_curve p)- (_pubkey pkp)- getECDSAScalarPrivateKey pubcurve- | _pkalgo pkp == ECDH- = do- pubcurve <- ecdhPrivateCurveFromPKPayload pkp- getECDHScalarPrivateKey pubcurve- | _pkalgo pkp == X25519 = do- if _keyVersion pkp == V6- then do- sk <- getByteString 32- return $ X25519PrivateKey sk- else do- pubcurve <- ecdhPrivateCurveFromPKPayload pkp- getECDHScalarPrivateKey pubcurve- | _pkalgo pkp == X448 = do- if _keyVersion pkp == V6- then do- sk <- getByteString 56- return $ X448PrivateKey sk- else UnknownSKey <$> getRemainingLazyByteString- | _pkalgo pkp == EdDSA = do- if _keyVersion pkp == V6- then do- case _pubkey pkp of- EdDSAPubKey P.Ed25519 _ -> EdDSAPrivateKey P.Ed25519 <$> getByteString 32- EdDSAPubKey P.Ed448 _ -> EdDSAPrivateKey P.Ed448 <$> getByteString 57- _ -> UnknownSKey <$> getRemainingLazyByteString- else do- MPI x <- get- case _pubkey pkp of- EdDSAPubKey P.Ed25519 _ ->- return $ EdDSAPrivateKey P.Ed25519 (leftPadTo 32 (i2osp x))- EdDSAPubKey P.Ed448 _ ->- return $ EdDSAPrivateKey P.Ed448 (leftPadTo 57 (i2osp x))- _ -> return $ UnknownSKey (BL.fromStrict (i2osp x))- | otherwise = UnknownSKey <$> getRemainingLazyByteString--getECDSAScalarPrivateKey :: ECCT.Curve -> Get SKey-getECDSAScalarPrivateKey curve = do- MPI pn <- get- pure $ ECDSAPrivateKey (ECDSA_PrivateKey (ECDSA.PrivateKey curve pn))--getECDHScalarPrivateKey :: ECCT.Curve -> Get SKey-getECDHScalarPrivateKey curve = do- MPI pn <- get- pure $ ECDHPrivateKey (ECDSA_PrivateKey (ECDSA.PrivateKey curve pn))--ecdhPrivateCurveFromPKPayload :: SomePKPayload -> Get ECCT.Curve-ecdhPrivateCurveFromPKPayload pkp =- case _pubkey pkp of- ECDHPubKey (ECDSAPubKey (ECDSA_PublicKey p)) _ _ ->- pure (ECDSA.public_curve p)- ECDHPubKey (EdDSAPubKey P.Ed25519 _) _ _ ->- pure (curve2Curve Curve25519)- ECDHPubKey (EdDSAPubKey P.Ed448 _) _ _ ->- pure (curve2Curve Curve448)- other ->- fail- ("ECDH/X25519 secret key requires an ECDH public key packet, got " ++- show other)--putSKey :: SKey -> Either String Put-putSKey (RSAPrivateKey (RSA_PrivateKey (R.PrivateKey _ d p q _ _ _))) =- case inverse q p of- Just u ->- Right (put (MPI d) >> put (MPI p) >> put (MPI q) >> put (MPI u))- Nothing ->- Left- "putSKey: invalid RSA key — q has no multiplicative inverse mod p (key is mathematically broken)"-putSKey (DSAPrivateKey (DSA_PrivateKey (D.PrivateKey _ x))) =- Right (put (MPI x))-putSKey (ElGamalPrivateKey x) =- Right (put (MPI x))-putSKey (ECDHPrivateKey (ECDSA_PrivateKey (ECDSA.PrivateKey _ d))) =- Right (put (MPI d))-putSKey (ECDSAPrivateKey (ECDSA_PrivateKey (ECDSA.PrivateKey _ d))) =- Right (put (MPI d))-putSKey (EdDSAPrivateKey P.Ed25519 sk) = Right (putByteString sk)-putSKey (EdDSAPrivateKey P.Ed448 sk) = Right (putByteString sk)-putSKey (X25519PrivateKey sk) = Right (putByteString sk)-putSKey (X448PrivateKey sk) = Right (putByteString sk)-putSKey (UnknownSKey bs) = Right (putLazyByteString bs)--putSKeyForPKPayload :: SomePKPayload -> SKey -> Either String Put-putSKeyForPKPayload pkp sk@(EdDSAPrivateKey _ bs)- | _keyVersion pkp == V6 = putSKey sk- | otherwise = Right (put (MPI (os2ip bs)))-putSKeyForPKPayload _ sk = putSKey sk--putMPI :: MPI -> Put-putMPI (MPI i) = do- let bs = i2osp i- putWord16be . fromIntegral . numBits $ i- putByteString bs--data PKPayloadReadCase where- PKPayloadReadCaseV3 :: V3Expiration -> PubKeyAlgorithm -> PKPayloadReadCase- PKPayloadReadCaseV4 :: PubKeyAlgorithm -> PKPayloadReadCase- PKPayloadReadCaseV6 :: PubKeyAlgorithm -> PKPayloadReadCase--pkPayloadReadCase :: Word8 -> Get PKPayloadReadCase-pkPayloadReadCase version =- case version of- 2 -> do- v3e <- getWord16be- pka <- get- pure (PKPayloadReadCaseV3 v3e pka)- 3 -> do- v3e <- getWord16be- pka <- get- pure (PKPayloadReadCaseV3 v3e pka)- 4 -> PKPayloadReadCaseV4 <$> get- 6 -> PKPayloadReadCaseV6 <$> get- _ -> fail ("unsupported key packet version " ++ show version)--getPKPayload :: Get SomePKPayload-getPKPayload = do- version <- getWord8- ctime <- fmap ThirtyTwoBitTimeStamp getWord32be- readCase <- pkPayloadReadCase version- case readCase of- PKPayloadReadCaseV3 v3e pka -> do- pk <- getPubkey pka- pure $! PKPayload DeprecatedV3 ctime v3e pka pk- PKPayloadReadCaseV4 pka -> do- pk <- getPubkey pka- pure $! PKPayload V4 ctime 0 pka pk- PKPayloadReadCaseV6 pka -> do- pk <- getPubkeyV6 pka- pure $! PKPayload V6 ctime 0 pka pk--data PKPayloadWriteCase where- PKPayloadWriteCaseV3 :: PKPayload 'DeprecatedV3 -> PKPayloadWriteCase- PKPayloadWriteCaseV4 :: PKPayload 'V4 -> PKPayloadWriteCase- PKPayloadWriteCaseV6 :: PKPayload 'V6 -> PKPayloadWriteCase--pkPayloadWriteCase :: SomePKPayload -> PKPayloadWriteCase-pkPayloadWriteCase (SomePKPayload pkp) =- case pkp of- PKPayloadV3 {} -> PKPayloadWriteCaseV3 pkp- PKPayloadV4 {} -> PKPayloadWriteCaseV4 pkp- PKPayloadV6 {} -> PKPayloadWriteCaseV6 pkp--putPKPayload :: SomePKPayload -> Put-putPKPayload pkpSome =- case pkPayloadWriteCase pkpSome of- PKPayloadWriteCaseV3 (PKPayloadV3 ctime v3e pka pk) -> do- putWord8 3- putWord32be . unThirtyTwoBitTimeStamp $ ctime- putWord16be v3e- put pka- putPubkey pk- PKPayloadWriteCaseV4 (PKPayloadV4 ctime pka pk) -> do- putWord8 4- putWord32be . unThirtyTwoBitTimeStamp $ ctime- put pka- putPubkeyV4ForAlgorithm pka pk- PKPayloadWriteCaseV6 (PKPayloadV6 ctime pka pk) -> do- putWord8 6- putWord32be . unThirtyTwoBitTimeStamp $ ctime- put pka- putPubkeyV6 pk--putPubkeyV4ForAlgorithm :: PubKeyAlgorithm -> PKey -> Put-putPubkeyV4ForAlgorithm pka pk- | pka == BTypes.Ed25519 = putPubkeyV4Fixed 32 P.Ed25519 pk- | pka == BTypes.Ed448 = putPubkeyV4Fixed 57 P.Ed448 pk- | pka == BTypes.X25519 = putPubkeyV4Fixed 32 P.Ed25519 pk- | pka == BTypes.X448 = putPubkeyV4Fixed 56 P.Ed448 pk- | otherwise = putPubkey pk--putPubkeyV4Fixed :: Int -> P.EdSigningCurve -> PKey -> Put-putPubkeyV4Fixed targetLen expectedCurve (EdDSAPubKey curve (NativeEPoint (EPoint x)))- | curve == expectedCurve = putByteString (fixedLengthOctets targetLen x)-putPubkeyV4Fixed _ _ pk = putPubkey pk--getSKAddendum :: SomePKPayload -> Get SKAddendum-getSKAddendum (SomePKPayload pkp) =- toSKAddendum <$> getSKAddendumTyped pkp--getSKAddendumTyped :: PKPayload v -> Get (SKAddendumV v)-getSKAddendumTyped pkp = do- s2kusage <- getWord8- let pkpSome = SomePKPayload pkp- getLegacyS2KProtected constructor = do- symencWord <- getWord8- s2k <- getS2K- let symenc = toFVal symencWord- case s2k of- OtherS2K _ _ -> return $ constructor symenc s2k mempty BL.empty- _ -> do- blockSize <- either fail pure (symEncBlockSize symenc)- iv <- IV <$> getByteString blockSize- encryptedblock <- getRemainingLazyByteString- return $ constructor symenc s2k iv encryptedblock- case s2kusage of- 0 ->- case pkp of- PKPayloadV6 {} -> do- sk <- getSecretKey pkpSome- return (SKAUnencryptedV6 sk)- PKPayloadV3 {} -> do- rest <- lookAhead getRemainingLazyByteString- secretLen <-- case runGetOrFail- (do- start <- bytesRead- _ <- getSecretKey pkpSome- end <- bytesRead- pure (end - start))- rest of- Left (_, _, err) -> fail err- Right (_, _, len) -> pure len- sk <- getSecretKey pkpSome- checksum <- getWord16be- let expectedChecksum =- checksum16Bytes (BL.toStrict (BL.take secretLen rest))- when (checksum /= expectedChecksum) $- fail- ("legacy unencrypted secret-key checksum mismatch: expected " ++- show expectedChecksum ++ ", got " ++ show checksum)- return (SKAUnencryptedLegacy sk checksum)- PKPayloadV4 {} -> do- rest <- lookAhead getRemainingLazyByteString- secretLen <-- case runGetOrFail- (do- start <- bytesRead- _ <- getSecretKey pkpSome- end <- bytesRead- pure (end - start))- rest of- Left (_, _, err) -> fail err- Right (_, _, len) -> pure len- sk <- getSecretKey pkpSome- checksum <- getWord16be- let expectedChecksum =- checksum16Bytes (BL.toStrict (BL.take secretLen rest))- when (checksum /= expectedChecksum) $- fail- ("legacy unencrypted secret-key checksum mismatch: expected " ++- show expectedChecksum ++ ", got " ++ show checksum)- return (SKAUnencryptedLegacy sk checksum)- 255 ->- case pkp of- PKPayloadV6 {} ->- fail "v6 secret key packets MUST NOT use s2k usage 255"- PKPayloadV3 {} ->- getLegacyS2KProtected SKA16bit- PKPayloadV4 {} ->- getLegacyS2KProtected SKA16bit- 254 ->- case pkp of- PKPayloadV6 {} -> do- paramsLen <- getWord8- params <- getLazyByteString (fromIntegral paramsLen)- (symenc, s2k, iv) <-- case runGetOrFail getV6CFBParams params of- Left (_, _, err) -> fail err- Right (rest, _, parsed)- | not (BL.null rest) -> fail "unexpected trailing v6 CFB parameters"- | otherwise -> pure parsed- encryptedblock <- getRemainingLazyByteString- return (SKASHA1V6 symenc s2k (IV iv) encryptedblock)- PKPayloadV3 {} ->- getLegacyS2KProtected SKASHA1Legacy- PKPayloadV4 {} ->- getLegacyS2KProtected SKASHA1Legacy- where- getV6CFBParams = do- symencWord <- getWord8- s2kLen <- getWord8- s2kBytes <- getLazyByteString (fromIntegral s2kLen)- s2k <-- case runGetOrFail getS2K s2kBytes of- Left (_, _, err) -> fail err- Right (rest, _, parsed)- | not (BL.null rest) -> fail "unexpected trailing bytes in v6 S2K specifier"- | otherwise -> pure parsed- iv <- getRemainingLazyByteString- let symenc = toFVal symencWord- blockSize <- either fail pure (symEncBlockSize symenc)- when (BL.length iv /= fromIntegral blockSize) $- fail "invalid v6 CFB IV length"- pure (symenc, s2k, BL.toStrict iv)- 253 ->- case pkp of- PKPayloadV6 {} -> do- paramsLen <- getWord8- params <- getLazyByteString (fromIntegral paramsLen)- (symenc, aead, s2k, iv) <-- case runGetOrFail getV6AEADParams params of- Left (_, _, err) -> fail err- Right (rest, _, parsed)- | not (BL.null rest) -> fail "unexpected trailing v6 AEAD parameters"- | otherwise -> pure parsed- encryptedblock <- getRemainingLazyByteString- return (SKAAEADV6 symenc aead s2k (IV iv) encryptedblock)- PKPayloadV3 {} -> do- (symenc, aead, s2k, iv) <- getLegacyAEADParams- encryptedblock <- getRemainingLazyByteString- return (SKAAEADLegacy symenc aead s2k (IV iv) encryptedblock)- PKPayloadV4 {} -> do- (symenc, aead, s2k, iv) <- getLegacyAEADParams- encryptedblock <- getRemainingLazyByteString- return (SKAAEADLegacy symenc aead s2k (IV iv) encryptedblock)- where- getV6AEADParams :: Get (SymmetricAlgorithm, AEADAlgorithm, S2K, B.ByteString)- getV6AEADParams = do- symencWord <- getWord8- aeadWord <- getWord8- s2kLen <- getWord8- s2kBytes <- getLazyByteString (fromIntegral s2kLen)- s2k <-- case runGetOrFail getS2K s2kBytes of- Left (_, _, err) -> fail err- Right (rest, _, parsed)- | not (BL.null rest) -> fail "unexpected trailing bytes in v6 S2K specifier"- | otherwise -> pure parsed- iv <- getRemainingLazyByteString- let symenc = toFVal symencWord- aead = toFVal aeadWord- when (BL.length iv /= fromIntegral (aeadNonceSize aead)) $- fail "invalid v6 AEAD IV length"- pure (symenc, aead, s2k, BL.toStrict iv)- -- v3/v4: no cumulative-params-length octet, no S2K-size octet- getLegacyAEADParams :: Get (SymmetricAlgorithm, AEADAlgorithm, S2K, B.ByteString)- getLegacyAEADParams = do- symencWord <- getWord8- aeadWord <- getWord8- s2k <- getS2K- let aead = toFVal aeadWord- iv <- BL.toStrict <$> getLazyByteString (fromIntegral (aeadNonceSize aead))- pure (toFVal symencWord, aead, s2k, iv)- symenc ->- case pkp of- PKPayloadV6 {} -> do- paramsLen <- getWord8- iv <- getByteString (fromIntegral paramsLen)- let symencAlg = toFVal symenc- blockSize <- either fail pure (symEncBlockSize symencAlg)- when (B.length iv /= blockSize) $- fail "invalid v6 CFB IV length"- encryptedblock <- getRemainingLazyByteString- return (SKASymV6 symencAlg (IV iv) encryptedblock)- PKPayloadV3 {} -> do- blockSize <- either fail pure (symEncBlockSize (toFVal symenc))- iv <- getByteString blockSize- encryptedblock <- getRemainingLazyByteString- return (SKASymLegacy (toFVal symenc) (IV iv) encryptedblock)- PKPayloadV4 {} -> do- blockSize <- either fail pure (symEncBlockSize (toFVal symenc))- iv <- getByteString blockSize- encryptedblock <- getRemainingLazyByteString- return (SKASymLegacy (toFVal symenc) (IV iv) encryptedblock)--putSKAddendum :: SKAddendum -> Either String Put-putSKAddendum (SUS16bit symenc s2k iv encryptedblock) = - Right $ do- putWord8 255- put symenc- put s2k- putByteString (unIV iv)- putLazyByteString encryptedblock-putSKAddendum (SUSSHA1 symenc s2k iv encryptedblock) = - Right $ do- putWord8 254- put symenc- put s2k- putByteString (unIV iv)- putLazyByteString encryptedblock-putSKAddendum (SUSAEAD symenc aead s2k iv encryptedblock) = - Right $ do- putWord8 253- put symenc- putWord8 (fromFVal aead)- put s2k- putByteString (unIV iv)- putLazyByteString encryptedblock-putSKAddendum (SUSym symenc iv encryptedblock) = - Right $ do- put symenc- putByteString (unIV iv)- putLazyByteString encryptedblock-putSKAddendum (SUUnencrypted sk checksum) = - do- putSecret <- putSKey sk- Right $ do- putWord8 0- let skb = runPut putSecret- putLazyByteString skb- putWord16be- (if checksum == 0- then checksum16Bytes (BL.toStrict skb)- else checksum)--checksum16Bytes :: B.ByteString -> Word16-checksum16Bytes =- B.foldl'- (\a b -> fromIntegral ((fromIntegral a + fromIntegral b) `mod` (65536 :: Integer)))- 0--putSKAddendumForPKPayload :: SomePKPayload -> SKAddendum -> Put-putSKAddendumForPKPayload pkp ska =- case fromSKAddendumForPKPayload pkp ska of- Left e -> error e- Right (SomeSKAddendumV skaV) ->- putSKAddendumForPKPayloadTyped pkp skaV--putSKAddendumForPKPayloadTyped ::- SomePKPayload- -> SKAddendumV v- -> Put-putSKAddendumForPKPayloadTyped pkp (SKAUnencryptedLegacy sk checksum) = do- putWord8 0- let putSecret =- case putSKeyForPKPayload pkp sk of- Left err -> error err- Right p -> p- skb = runPut putSecret- putLazyByteString skb- putWord16be- (if checksum == 0- then BL.foldl (\a b -> mod (a + fromIntegral b) 0xffff) (0 :: Word16) skb- else checksum)-putSKAddendumForPKPayloadTyped pkp (SKAUnencryptedV6 sk) = do- putWord8 0- let putSecret =- case putSKeyForPKPayload pkp sk of- Left err -> error err- Right p -> p- skb = runPut putSecret- putLazyByteString skb-putSKAddendumForPKPayloadTyped _ (SKASHA1V6 symenc s2k iv encryptedblock) = do- let s2kbs = runPut (put s2k)- paramsLen = 1 + 1 + BL.length s2kbs + fromIntegral (B.length (unIV iv))- putWord8 254- putWord8 (fromIntegral paramsLen)- put symenc- putWord8 (fromIntegral (BL.length s2kbs))- putLazyByteString s2kbs- putByteString (unIV iv)- putLazyByteString encryptedblock-putSKAddendumForPKPayloadTyped _ (SKAAEADV6 symenc aead s2k iv encryptedblock) = do- let s2kbs = runPut (put s2k)- paramsLen = 1 + 1 + 1 + BL.length s2kbs + fromIntegral (B.length (unIV iv))- putWord8 253- putWord8 (fromIntegral paramsLen)- put symenc- putWord8 (fromFVal aead)- putWord8 (fromIntegral (BL.length s2kbs))- putLazyByteString s2kbs- putByteString (unIV iv)- putLazyByteString encryptedblock-putSKAddendumForPKPayloadTyped _ (SKAAEADLegacy symenc aead s2k iv encryptedblock) = do- putWord8 253- put symenc- putWord8 (fromFVal aead)- put s2k- putByteString (unIV iv)- putLazyByteString encryptedblock-putSKAddendumForPKPayloadTyped _ (SKASymV6 symenc iv encryptedblock) = do- putWord8 (fromFVal symenc)- putWord8 (fromIntegral (B.length (unIV iv)))- putByteString (unIV iv)- putLazyByteString encryptedblock-putSKAddendumForPKPayloadTyped _ skaV =- case putSKAddendum (toSKAddendum skaV) of- Left e -> error e- Right p -> p--aeadNonceSize :: AEADAlgorithm -> Int-aeadNonceSize EAX = 16-aeadNonceSize OCB = 15-aeadNonceSize GCM = 12-aeadNonceSize (OtherAEADAlgo _) = 0--symEncBlockSize :: SymmetricAlgorithm -> Either String Int-symEncBlockSize Plaintext = Right 0-symEncBlockSize IDEA = Right 8-symEncBlockSize TripleDES = Right 8-symEncBlockSize CAST5 = Right 8-symEncBlockSize Blowfish = Right 8-symEncBlockSize AES128 = Right 16-symEncBlockSize AES192 = Right 16-symEncBlockSize AES256 = Right 16-symEncBlockSize Twofish = Right 16-symEncBlockSize Camellia128 = Right 16-symEncBlockSize Camellia192 = Right 16-symEncBlockSize Camellia256 = Right 16-symEncBlockSize sa =- Left ("unsupported symmetric algorithm for secret-key IV sizing: " ++ show sa)--decodeIterationCount :: Word8 -> IterationCount-decodeIterationCount c =- IterationCount- ((16 + (fromIntegral c .&. 15)) `shiftL` ((fromIntegral c `shiftR` 4) + 6))--encodeIterationCount :: IterationCount -> Word8 -- should this really be a lookup table?-encodeIterationCount 1024 = 0-encodeIterationCount 1088 = 1-encodeIterationCount 1152 = 2-encodeIterationCount 1216 = 3-encodeIterationCount 1280 = 4-encodeIterationCount 1344 = 5-encodeIterationCount 1408 = 6-encodeIterationCount 1472 = 7-encodeIterationCount 1536 = 8-encodeIterationCount 1600 = 9-encodeIterationCount 1664 = 10-encodeIterationCount 1728 = 11-encodeIterationCount 1792 = 12-encodeIterationCount 1856 = 13-encodeIterationCount 1920 = 14-encodeIterationCount 1984 = 15-encodeIterationCount 2048 = 16-encodeIterationCount 2176 = 17-encodeIterationCount 2304 = 18-encodeIterationCount 2432 = 19-encodeIterationCount 2560 = 20-encodeIterationCount 2688 = 21-encodeIterationCount 2816 = 22-encodeIterationCount 2944 = 23-encodeIterationCount 3072 = 24-encodeIterationCount 3200 = 25-encodeIterationCount 3328 = 26-encodeIterationCount 3456 = 27-encodeIterationCount 3584 = 28-encodeIterationCount 3712 = 29-encodeIterationCount 3840 = 30-encodeIterationCount 3968 = 31-encodeIterationCount 4096 = 32-encodeIterationCount 4352 = 33-encodeIterationCount 4608 = 34-encodeIterationCount 4864 = 35-encodeIterationCount 5120 = 36-encodeIterationCount 5376 = 37-encodeIterationCount 5632 = 38-encodeIterationCount 5888 = 39-encodeIterationCount 6144 = 40-encodeIterationCount 6400 = 41-encodeIterationCount 6656 = 42-encodeIterationCount 6912 = 43-encodeIterationCount 7168 = 44-encodeIterationCount 7424 = 45-encodeIterationCount 7680 = 46-encodeIterationCount 7936 = 47-encodeIterationCount 8192 = 48-encodeIterationCount 8704 = 49-encodeIterationCount 9216 = 50-encodeIterationCount 9728 = 51-encodeIterationCount 10240 = 52-encodeIterationCount 10752 = 53-encodeIterationCount 11264 = 54-encodeIterationCount 11776 = 55-encodeIterationCount 12288 = 56-encodeIterationCount 12800 = 57-encodeIterationCount 13312 = 58-encodeIterationCount 13824 = 59-encodeIterationCount 14336 = 60-encodeIterationCount 14848 = 61-encodeIterationCount 15360 = 62-encodeIterationCount 15872 = 63-encodeIterationCount 16384 = 64-encodeIterationCount 17408 = 65-encodeIterationCount 18432 = 66-encodeIterationCount 19456 = 67-encodeIterationCount 20480 = 68-encodeIterationCount 21504 = 69-encodeIterationCount 22528 = 70-encodeIterationCount 23552 = 71-encodeIterationCount 24576 = 72-encodeIterationCount 25600 = 73-encodeIterationCount 26624 = 74-encodeIterationCount 27648 = 75-encodeIterationCount 28672 = 76-encodeIterationCount 29696 = 77-encodeIterationCount 30720 = 78-encodeIterationCount 31744 = 79-encodeIterationCount 32768 = 80-encodeIterationCount 34816 = 81-encodeIterationCount 36864 = 82-encodeIterationCount 38912 = 83-encodeIterationCount 40960 = 84-encodeIterationCount 43008 = 85-encodeIterationCount 45056 = 86-encodeIterationCount 47104 = 87-encodeIterationCount 49152 = 88-encodeIterationCount 51200 = 89-encodeIterationCount 53248 = 90-encodeIterationCount 55296 = 91-encodeIterationCount 57344 = 92-encodeIterationCount 59392 = 93-encodeIterationCount 61440 = 94-encodeIterationCount 63488 = 95-encodeIterationCount 65536 = 96-encodeIterationCount 69632 = 97-encodeIterationCount 73728 = 98-encodeIterationCount 77824 = 99-encodeIterationCount 81920 = 100-encodeIterationCount 86016 = 101-encodeIterationCount 90112 = 102-encodeIterationCount 94208 = 103-encodeIterationCount 98304 = 104-encodeIterationCount 102400 = 105-encodeIterationCount 106496 = 106-encodeIterationCount 110592 = 107-encodeIterationCount 114688 = 108-encodeIterationCount 118784 = 109-encodeIterationCount 122880 = 110-encodeIterationCount 126976 = 111-encodeIterationCount 131072 = 112-encodeIterationCount 139264 = 113-encodeIterationCount 147456 = 114-encodeIterationCount 155648 = 115-encodeIterationCount 163840 = 116-encodeIterationCount 172032 = 117-encodeIterationCount 180224 = 118-encodeIterationCount 188416 = 119-encodeIterationCount 196608 = 120-encodeIterationCount 204800 = 121-encodeIterationCount 212992 = 122-encodeIterationCount 221184 = 123-encodeIterationCount 229376 = 124-encodeIterationCount 237568 = 125-encodeIterationCount 245760 = 126-encodeIterationCount 253952 = 127-encodeIterationCount 262144 = 128-encodeIterationCount 278528 = 129-encodeIterationCount 294912 = 130-encodeIterationCount 311296 = 131-encodeIterationCount 327680 = 132-encodeIterationCount 344064 = 133-encodeIterationCount 360448 = 134-encodeIterationCount 376832 = 135-encodeIterationCount 393216 = 136-encodeIterationCount 409600 = 137-encodeIterationCount 425984 = 138-encodeIterationCount 442368 = 139-encodeIterationCount 458752 = 140-encodeIterationCount 475136 = 141-encodeIterationCount 491520 = 142-encodeIterationCount 507904 = 143-encodeIterationCount 524288 = 144-encodeIterationCount 557056 = 145-encodeIterationCount 589824 = 146-encodeIterationCount 622592 = 147-encodeIterationCount 655360 = 148-encodeIterationCount 688128 = 149-encodeIterationCount 720896 = 150-encodeIterationCount 753664 = 151-encodeIterationCount 786432 = 152-encodeIterationCount 819200 = 153-encodeIterationCount 851968 = 154-encodeIterationCount 884736 = 155-encodeIterationCount 917504 = 156-encodeIterationCount 950272 = 157-encodeIterationCount 983040 = 158-encodeIterationCount 1015808 = 159-encodeIterationCount 1048576 = 160-encodeIterationCount 1114112 = 161-encodeIterationCount 1179648 = 162-encodeIterationCount 1245184 = 163-encodeIterationCount 1310720 = 164-encodeIterationCount 1376256 = 165-encodeIterationCount 1441792 = 166-encodeIterationCount 1507328 = 167-encodeIterationCount 1572864 = 168-encodeIterationCount 1638400 = 169-encodeIterationCount 1703936 = 170-encodeIterationCount 1769472 = 171-encodeIterationCount 1835008 = 172-encodeIterationCount 1900544 = 173-encodeIterationCount 1966080 = 174-encodeIterationCount 2031616 = 175-encodeIterationCount 2097152 = 176-encodeIterationCount 2228224 = 177-encodeIterationCount 2359296 = 178-encodeIterationCount 2490368 = 179-encodeIterationCount 2621440 = 180-encodeIterationCount 2752512 = 181-encodeIterationCount 2883584 = 182-encodeIterationCount 3014656 = 183-encodeIterationCount 3145728 = 184-encodeIterationCount 3276800 = 185-encodeIterationCount 3407872 = 186-encodeIterationCount 3538944 = 187-encodeIterationCount 3670016 = 188-encodeIterationCount 3801088 = 189-encodeIterationCount 3932160 = 190-encodeIterationCount 4063232 = 191-encodeIterationCount 4194304 = 192-encodeIterationCount 4456448 = 193-encodeIterationCount 4718592 = 194-encodeIterationCount 4980736 = 195-encodeIterationCount 5242880 = 196-encodeIterationCount 5505024 = 197-encodeIterationCount 5767168 = 198-encodeIterationCount 6029312 = 199-encodeIterationCount 6291456 = 200-encodeIterationCount 6553600 = 201-encodeIterationCount 6815744 = 202-encodeIterationCount 7077888 = 203-encodeIterationCount 7340032 = 204-encodeIterationCount 7602176 = 205-encodeIterationCount 7864320 = 206-encodeIterationCount 8126464 = 207-encodeIterationCount 8388608 = 208-encodeIterationCount 8912896 = 209-encodeIterationCount 9437184 = 210-encodeIterationCount 9961472 = 211-encodeIterationCount 10485760 = 212-encodeIterationCount 11010048 = 213-encodeIterationCount 11534336 = 214-encodeIterationCount 12058624 = 215-encodeIterationCount 12582912 = 216-encodeIterationCount 13107200 = 217-encodeIterationCount 13631488 = 218-encodeIterationCount 14155776 = 219-encodeIterationCount 14680064 = 220-encodeIterationCount 15204352 = 221-encodeIterationCount 15728640 = 222-encodeIterationCount 16252928 = 223-encodeIterationCount 16777216 = 224-encodeIterationCount 17825792 = 225-encodeIterationCount 18874368 = 226-encodeIterationCount 19922944 = 227-encodeIterationCount 20971520 = 228-encodeIterationCount 22020096 = 229-encodeIterationCount 23068672 = 230-encodeIterationCount 24117248 = 231-encodeIterationCount 25165824 = 232-encodeIterationCount 26214400 = 233-encodeIterationCount 27262976 = 234-encodeIterationCount 28311552 = 235-encodeIterationCount 29360128 = 236-encodeIterationCount 30408704 = 237-encodeIterationCount 31457280 = 238-encodeIterationCount 32505856 = 239-encodeIterationCount 33554432 = 240-encodeIterationCount 35651584 = 241-encodeIterationCount 37748736 = 242-encodeIterationCount 39845888 = 243-encodeIterationCount 41943040 = 244-encodeIterationCount 44040192 = 245-encodeIterationCount 46137344 = 246-encodeIterationCount 48234496 = 247-encodeIterationCount 50331648 = 248-encodeIterationCount 52428800 = 249-encodeIterationCount 54525952 = 250-encodeIterationCount 56623104 = 251-encodeIterationCount 58720256 = 252-encodeIterationCount 60817408 = 253-encodeIterationCount 62914560 = 254-encodeIterationCount 65011712 = 255-encodeIterationCount n = error ("invalid iteration count" ++ show n)--getSignaturePayload :: Get SignaturePayload-getSignaturePayload = do- pv <- getWord8- case pv of- 3 -> do- hashlen <- getWord8- guard (hashlen == 5)- st <- getWord8- ctime <- fmap ThirtyTwoBitTimeStamp getWord32be- eok <- getLazyByteString 8- pka <- get- ha <- get- left16 <- getWord16be- mpib <- getRemainingLazyByteString- case runGetOrFail (some getMPI) mpib of- Left (_, _, e) -> fail ("v3 sig MPIs " ++ e)- Right (_, _, mpis) ->- return $- SigV3- (toFVal st)- ctime- (EightOctetKeyId eok)- (toFVal pka)- (toFVal ha)- left16- (NE.fromList mpis)- 4 -> do- st <- getWord8- pkaOctet <- get- ha <- get- let pka = toFVal pkaOctet :: PubKeyAlgorithm- hlen <- getWord16be- hb <- getLazyByteString (fromIntegral hlen)- let hashed =- case runGetOrFail (many getSigSubPacket) hb of- Left (_, _, err) -> fail ("v4 sig hasheds " ++ err)- Right (_, _, h) -> h- ulen <- getWord16be- ub <- getLazyByteString (fromIntegral ulen)- let unhashed =- case runGetOrFail (many getSigSubPacket) ub of- Left (_, _, err) -> fail ("v4 sig unhasheds " ++ err)- Right (_, _, u) -> u- left16 <- getWord16be- mpib <- getRemainingLazyByteString- let parseV4MPIs parseErrPrefix =- case runGetOrFail (some getMPI) mpib of- Left (_, _, e) -> fail (parseErrPrefix ++ e)- Right (_, _, mpis) ->- return $- SigV4- (toFVal st)- pka- (toFVal ha)- hashed- unhashed- left16- (NE.fromList mpis)- if pka == BTypes.Ed25519- then- if BL.length mpib == 64- then do- let sig = BL.toStrict mpib- (rbs, sbs) = B.splitAt 32 sig- return $- SigV4- (toFVal st)- pka- (toFVal ha)- hashed- unhashed- left16- (NE.fromList [MPI (os2ip rbs), MPI (os2ip sbs)])- else parseV4MPIs "v4 Ed25519 legacy MPIs "- else- if pka == BTypes.Ed448- then- if BL.length mpib == 114- then do- let sig = BL.toStrict mpib- (rbs, sbs) = B.splitAt 57 sig- return $- SigV4- (toFVal st)- pka- (toFVal ha)- hashed- unhashed- left16- (NE.fromList [MPI (os2ip rbs), MPI (os2ip sbs)])- else parseV4MPIs "v4 Ed448 legacy MPIs "- else parseV4MPIs "v4 sig MPIs "- 6 -> do- st <- getWord8- pka <- get- ha <- get- hlen <- getWord32be- hb <- getLazyByteString (fromIntegral hlen)- let hashed =- case runGetOrFail (many getSigSubPacket) hb of- Left (_, _, err) -> fail ("v6 sig hasheds " ++ err)- Right (_, _, h) -> h- ulen <- getWord32be- ub <- getLazyByteString (fromIntegral ulen)- let unhashed =- case runGetOrFail (many getSigSubPacket) ub of- Left (_, _, err) -> fail ("v6 sig unhasheds " ++ err)- Right (_, _, u) -> u- left16 <- getWord16be- saltSize <- getWord8- let haVal = (toFVal ha :: HashAlgorithm)- expectedSaltSize <-- maybe- (fail ("signature hash algorithm does not define a V6 salt size: " ++ show haVal))- pure- (v6SaltSizeForHashAlgorithm haVal)- when (saltSize /= expectedSaltSize) $- fail- ("v6 signature salt size mismatch for " ++- show haVal ++ ": expected " ++ show expectedSaltSize ++ ", got " ++ show saltSize)- saltbs <- getByteString (fromIntegral saltSize)- let salt = SignatureSalt (BL.fromStrict saltbs)- if pka == BTypes.Ed25519- then do- sig <- getByteString 64- let (rbs, sbs) = B.splitAt 32 sig- mpis = [MPI (os2ip rbs), MPI (os2ip sbs)]- return $- SigV6- (toFVal st)- pka- (toFVal ha)- salt- hashed- unhashed- left16- (NE.fromList mpis)- else- if pka == BTypes.Ed448- then do- sig <- getByteString 114- let (rbs, sbs) = B.splitAt 57 sig- mpis = [MPI (os2ip rbs), MPI (os2ip sbs)]- return $- SigV6- (toFVal st)- pka- (toFVal ha)- salt- hashed- unhashed- left16- (NE.fromList mpis)- else do- mpib <- getRemainingLazyByteString- case runGetOrFail (some getMPI) mpib of- Left (_, _, e) -> fail ("v6 sig MPIs " ++ e)- Right (_, _, mpis) ->- return $- SigV6- (toFVal st)- pka- (toFVal ha)- salt- hashed- unhashed- left16- (NE.fromList mpis)- _ -> do- bs <- getRemainingLazyByteString- return $ SigVOther pv bs--putSignaturePayload :: SignaturePayload -> Put-putSignaturePayload (SigV3 st ctime eok pka ha left16 mpis) = do- putWord8 3- putWord8 5 -- hashlen- put st- putWord32be . unThirtyTwoBitTimeStamp $ ctime- putLazyByteString (unEOKI eok)- put pka- put ha- putWord16be left16- F.mapM_ put mpis-putSignaturePayload (SigV4 st pka ha hashed unhashed left16 mpis) = do- putWord8 4- put st- put pka- put ha- let hb = runPut $ mapM_ put hashed- putWord16be . fromIntegral . BL.length $ hb- putLazyByteString hb- let ub = runPut $ mapM_ put unhashed- putWord16be . fromIntegral . BL.length $ ub- putLazyByteString ub- putWord16be left16- if pka == BTypes.Ed25519- then- case NE.toList mpis of- [MPI r, MPI s] -> do- putByteString (padN 32 r)- putByteString (padN 32 s)- _ -> error "Ed25519 v4 signatures must have two MPIs"- else- if pka == BTypes.Ed448- then- case NE.toList mpis of- [MPI r, MPI s] -> do- putByteString (padN 57 r)- putByteString (padN 57 s)- _ -> error "Ed448 v4 signatures must have two MPIs"- else F.mapM_ put mpis- where- padN n i =- let bs = i2osp i- in B.replicate (max 0 (n - B.length bs)) 0 <> bs-putSignaturePayload (SigV6 st pka ha salt hashed unhashed left16 mpis) = do- let expectedSaltSize =- maybe- (error ("signature hash algorithm does not define a V6 salt size: " ++ show ha))- id- (v6SaltSizeForHashAlgorithm ha)- actualSaltSize = fromIntegral (BL.length (unSignatureSalt salt))- when (actualSaltSize /= expectedSaltSize) $- error- ("v6 signature salt size mismatch for " ++- show ha ++ ": expected " ++ show expectedSaltSize ++ ", got " ++ show actualSaltSize)- putWord8 6- put st- put pka- put ha- let hb = runPut $ mapM_ put hashed- putWord32be . fromIntegral . BL.length $ hb- putLazyByteString hb- let ub = runPut $ mapM_ put unhashed- putWord32be . fromIntegral . BL.length $ ub- putLazyByteString ub- putWord16be left16- putWord8 . fromIntegral . BL.length . unSignatureSalt $ salt- putByteString (BL.toStrict (unSignatureSalt salt))- if pka == BTypes.Ed25519- then- case NE.toList mpis of- [MPI r, MPI s] -> do- putByteString (padN 32 r)- putByteString (padN 32 s)- _ -> error "Ed25519 v6 signatures must have two MPIs"- else- if pka == BTypes.Ed448- then- case NE.toList mpis of- [MPI r, MPI s] -> do- putByteString (padN 57 r)- putByteString (padN 57 s)- _ -> error "Ed448 v6 signatures must have two MPIs"- else F.mapM_ put mpis- where- padN n i =- let bs = i2osp i- in B.replicate (max 0 (n - B.length bs)) 0 <> bs-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_ 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]-parsePkts = reverse . fst . parsePktsAccum 0 []---- | Parse packets from a ByteString and report the first parse failure.-parsePktsEither :: ByteString -> Either PktParseError [Pkt]-parsePktsEither lbs =- case parsePktsAccum 0 [] lbs of- (pkts, Nothing) -> Right (reverse pkts)- (_, Just err) -> Left err--data PktParseError =- PktParseError- { pktParseErrorOffset :: Int64- , pktParseErrorMessage :: String- }- deriving (Eq, Show)--parsePktsAccum ::- Int64 -> [Pkt] -> ByteString -> ([Pkt], Maybe PktParseError)-parsePktsAccum offset acc lbs- | BL.null lbs = (acc, Nothing)- | otherwise =- case runGetOrFail getPkt lbs of- Left (_, parseOffset, msg) -> (acc, err parseOffset msg)- Right (rest, consumed, pkt) ->- parsePktsAccum (offset + consumed) (pkt : acc) rest- where- err parseOffset msg =- Just- PktParseError- { pktParseErrorOffset = offset + parseOffset- , pktParseErrorMessage = msg- }--armorPayloads :: [Armor] -> [ByteString]-armorPayloads =- foldr collect []- where- collect (Armor _ _ payload) = (BL.fromStrict (BLC8.toStrict payload) :)- collect (ClearSigned _ _ inner) = (armorPayloads [inner] ++)--looksLikeAsciiArmor :: ByteString -> Bool-looksLikeAsciiArmor =- (armorHeaderLazy `BL.isPrefixOf`) . BL.dropWhile isLeadingArmorWhitespace--looksLikeAsciiArmorLenient :: ByteString -> Bool-looksLikeAsciiArmorLenient = looksLikeAsciiArmor . stripUtf8Bom--armorPayloadsOfType :: ArmorType -> [Armor] -> [ByteString]-armorPayloadsOfType atype =- foldr collect []- where- collect (Armor innerType _ payload)- | innerType == atype = (BL.fromStrict (BLC8.toStrict payload) :)- | otherwise = id- collect (ClearSigned _ _ inner) = (armorPayloadsOfType atype [inner] ++)--singleArmorPayloadOfType :: ArmorType -> [Armor] -> Either String ByteString-singleArmorPayloadOfType atype armors =- case armorPayloadsOfType atype armors of- [payload] -> Right payload- [] -> Left ("ASCII armor decode returned no " ++ show atype ++ " blocks")- payloads ->- Left- ("ASCII armor decode returned " ++- show (length payloads) ++ " " ++ show atype ++ " blocks (expected exactly one)")--singleClearSignedBlock ::- [Armor] -> Either String ([(String, String)], ByteString, ByteString)-singleClearSignedBlock armors =- case [clearSignedBlockFromArmor armor | armor@ClearSigned {} <- armors] of- [Right clearSigned] -> Right clearSigned- [Left err] -> Left err- [] -> Left "ASCII armor decode returned no clear-signed blocks"- clearSigneds ->- Left- ("ASCII armor decode returned " ++- show (length clearSigneds) ++ " clear-signed blocks (expected exactly one)")- where- clearSignedBlockFromArmor (ClearSigned hs cleartext inner) =- case inner of- Armor ArmorSignature _ sig ->- Right- ( hs- , BL.fromStrict (BLC8.toStrict cleartext)- , BL.fromStrict (BLC8.toStrict sig)- )- Armor atype _ _ ->- Left- ("clear-signed block contained inner armor type " ++- show atype ++ " (expected ArmorSignature)")- ClearSigned {} ->- Left "clear-signed block contained nested clear-signed payload"- clearSignedBlockFromArmor _ =- Left "internal error: expected ClearSigned armor block"--recommendedArmorType :: [Pkt] -> Maybe ArmorType-recommendedArmorType [] = Nothing-recommendedArmorType (pkt:_)- | isPrivateKeyPacket pkt = Just ArmorPrivateKeyBlock- | isPublicKeyPacket pkt = Just ArmorPublicKeyBlock- | isSignaturePacket pkt = Just ArmorSignature- | otherwise = Just ArmorMessage- where- isPrivateKeyPacket SecretKeyPkt {} = True- isPrivateKeyPacket SecretSubkeyPkt {} = True- isPrivateKeyPacket _ = False-- isPublicKeyPacket PublicKeyPkt {} = True- isPublicKeyPacket PublicSubkeyPkt {} = True- isPublicKeyPacket _ = False-- isSignaturePacket SignaturePkt {} = True- isSignaturePacket _ = False--dearmorIfAsciiArmored :: ByteString -> Either String (Bool, ByteString)-dearmorIfAsciiArmored bs- | looksLikeAsciiArmor bs =- (\payload -> (True, payload)) <$> decodeSingleArmorPayload bs- | otherwise = Right (False, bs)--dearmorIfAsciiArmoredLenient :: ByteString -> Either String (Bool, ByteString)-dearmorIfAsciiArmoredLenient bs- | looksLikeAsciiArmorLenient bs =- (\payload -> (True, payload)) <$> decodeSingleArmorPayloadLenient (stripUtf8Bom bs)- | otherwise = Right (False, bs)--decodeSingleArmorPayload :: ByteString -> Either String ByteString-decodeSingleArmorPayload bs =- case AA.decodeLazy bs of- Left err -> Left err- Right armors -> singleArmorPayload armors--decodeSingleArmorPayloadLenient :: ByteString -> Either String ByteString-decodeSingleArmorPayloadLenient bs =- case decodeSingleArmorPayload bs of- Right payload -> Right payload- Left strictErr ->- let normalized = normalizeAsciiArmorForLenientDecode bs- in if normalized == bs- then Left strictErr- else case decodeSingleArmorPayload normalized of- Left lenientErr ->- Left- (strictErr ++- " (lenient normalization retry failed: " ++ lenientErr ++ ")")- Right payload -> Right payload--singleArmorPayload :: [Armor] -> Either String ByteString-singleArmorPayload armors =- case armorPayloads armors of- [payload] -> Right payload- [] -> Left "ASCII armor decode succeeded but returned no blocks"- payloads ->- Left- ("ASCII armor decode returned " ++- show (length payloads) ++ " blocks (expected exactly one)")--data WireRepInput =- WireRepInput- { wireRepInputRef :: WireRepRef- , wireRepInputPayload :: ByteString- }--wireRepRefFromInput :: Maybe T.Text -> ByteString -> Either String WireRepInput-wireRepRefFromInput mname bs =- (\(wasArmored, payload) ->- WireRepInput- { wireRepInputRef = BTypes.mkWireRepRef mname wasArmored payload- , wireRepInputPayload = payload- }) <$>- dearmorIfAsciiArmored bs--data ParseState =- ParseState- { psOffset :: Int64- , psIndex :: Int- , psSource :: WireRepRef- , psRemaining :: BL.ByteString- }---- | Parse packets from a source bytestream, preserving packet provenance.-parsePktsWithWireRep :: WireRepRef -> ByteString -> [PktWithWireRep]-parsePktsWithWireRep src input = go initialState- where- initialState =- ParseState- { psOffset = 0- , psIndex = 0- , psSource = src- , psRemaining = input- }- go state- | BL.null (psRemaining state) = []- | otherwise =- case runGetOrFail getPkt (psRemaining state) of- Left (_, _, _) -> []- Right (rest, consumed, pkt) ->- let raw = BL.take consumed (psRemaining state)- newState =- state- { psOffset = psOffset state + consumed- , psIndex = psIndex state + 1- , psRemaining = rest- }- pktWithSource =- PktWithWireRep- (psSource state)- (ByteRange (psOffset state) consumed)- raw- (psIndex state)- pkt- in pktWithSource : go newState--conduitParsePktsWithWireRep ::- Monad m => Maybe T.Text -> ConduitT B.ByteString PktWithWireRep m ()-conduitParsePktsWithWireRep mname = go (UndecidedInput [])- where- go !state = do- mchunk <- await- case mchunk of- Nothing -> mapM_ yield (finishConduitState mname state)- Just chunk ->- let !nextState = consumeConduitChunk chunk state- in go nextState--data ConduitParseState- = UndecidedInput ![B.ByteString]- | ArmoredInput ![B.ByteString]- | BinaryInput !BinaryParseState--data BinaryParseState =- BinaryParseState- { bpsLength :: !Int64- , bpsOffset :: !Int64- , bpsIndex :: !Int- , bpsBuffer :: !B.ByteString- , bpsParsedRev :: [ParsedPacketChunk]- }--data ArmorPrefixDecision- = PrefixNeedsMore- | PrefixIsArmored- | PrefixIsBinary--consumeConduitChunk :: B.ByteString -> ConduitParseState -> ConduitParseState-consumeConduitChunk chunk (UndecidedInput chunksRev) =- let prefixChunksRev = chunk : chunksRev- prefix = B.concat (reverse prefixChunksRev)- in case classifyArmorPrefix prefix of- PrefixNeedsMore -> UndecidedInput prefixChunksRev- PrefixIsArmored -> ArmoredInput prefixChunksRev- PrefixIsBinary -> feedBinaryChunk prefix initialBinaryParseState-consumeConduitChunk chunk (ArmoredInput chunksRev) = ArmoredInput (chunk : chunksRev)-consumeConduitChunk chunk (BinaryInput state) = BinaryInput (advanceBinaryParseState chunk state)--finishConduitState :: Maybe T.Text -> ConduitParseState -> [PktWithWireRep]-finishConduitState mname (UndecidedInput chunksRev) =- finalizeBinaryParseState- mname- (advanceBinaryParseState (B.concat (reverse chunksRev)) initialBinaryParseState)-finishConduitState mname (ArmoredInput chunksRev) =- let input = BL.fromChunks (reverse chunksRev)- in case wireRepRefFromInput mname input of- Left _ -> []- Right WireRepInput- { wireRepInputRef = src- , wireRepInputPayload = payload- } ->- parsePktsWithWireRep src payload-finishConduitState mname (BinaryInput state) = finalizeBinaryParseState mname state--initialBinaryParseState :: BinaryParseState-initialBinaryParseState =- BinaryParseState- { bpsLength = 0- , bpsOffset = 0- , bpsIndex = 0- , bpsBuffer = B.empty- , bpsParsedRev = []- }--feedBinaryChunk :: B.ByteString -> BinaryParseState -> ConduitParseState-feedBinaryChunk chunk = BinaryInput . advanceBinaryParseState chunk--advanceBinaryParseState :: B.ByteString -> BinaryParseState -> BinaryParseState-advanceBinaryParseState chunk state =- let !nextLength = bpsLength state + fromIntegral (B.length chunk)- !(nextOffset, nextIndex, nextBuffer, nextParsedRev) =- drainParsedPackets- (bpsOffset state)- (bpsIndex state)- (bpsBuffer state <> chunk)- (bpsParsedRev state)- in BinaryParseState- { bpsLength = nextLength- , bpsOffset = nextOffset- , bpsIndex = nextIndex- , bpsBuffer = nextBuffer- , bpsParsedRev = nextParsedRev- }--finalizeBinaryParseState :: Maybe T.Text -> BinaryParseState -> [PktWithWireRep]-finalizeBinaryParseState mname state =- let src = BTypes.mkWireRepRefWithLength mname False (bpsLength state)- in map (toPktWithWireRep src) (reverse (bpsParsedRev state))--classifyArmorPrefix :: B.ByteString -> ArmorPrefixDecision-classifyArmorPrefix prefix =- case B.dropWhile isLeadingArmorWhitespace prefix of- rest- | B.null rest -> PrefixNeedsMore- | armorHeader `B.isPrefixOf` rest -> PrefixIsArmored- | rest `B.isPrefixOf` armorHeader -> PrefixNeedsMore- | otherwise -> PrefixIsBinary--armorHeader :: B.ByteString-armorHeader = B.pack (map (fromIntegral . fromEnum) "-----BEGIN PGP ")--armorHeaderLazy :: ByteString-armorHeaderLazy = BL.fromStrict armorHeader--utf8Bom :: ByteString-utf8Bom = BL.pack [0xef, 0xbb, 0xbf]--isLeadingArmorWhitespace :: Word8 -> Bool-isLeadingArmorWhitespace w = w == 0x20 || w == 0x09 || w == 0x0d || w == 0x0a--stripUtf8Bom :: ByteString -> ByteString-stripUtf8Bom bs- | utf8Bom `BL.isPrefixOf` bs = BL.drop (fromIntegral (BL.length utf8Bom)) bs- | otherwise = bs--normalizeAsciiArmorForLenientDecode :: ByteString -> ByteString-normalizeAsciiArmorForLenientDecode =- ensureTrailingLf . normalizeLineEndings- where- ensureTrailingLf lbs- | BL.null lbs = lbs- | BL.last lbs == 0x0a = lbs- | otherwise = lbs <> BL.singleton 0x0a- normalizeLineEndings lbs =- case BL.uncons lbs of- Nothing -> BL.empty- Just (0x0d, rest) ->- case BL.uncons rest of- Just (0x0a, rest') -> BL.cons 0x0a (normalizeLineEndings rest')- _ -> BL.cons 0x0a (normalizeLineEndings rest)- Just (w, rest) -> BL.cons w (normalizeLineEndings rest)--data ParsedPacketChunk =- ParsedPacketChunk- { ppcRange :: ByteRange- , ppcRaw :: ByteString- , ppcIndex :: Int- , ppcValue :: Pkt- }--toPktWithWireRep :: WireRepRef -> ParsedPacketChunk -> PktWithWireRep-toPktWithWireRep src ppc =- PktWithWireRep src (ppcRange ppc) (ppcRaw ppc) (ppcIndex ppc) (ppcValue ppc)--drainParsedPackets ::- Int64- -> Int- -> B.ByteString- -> [ParsedPacketChunk]- -> (Int64, Int, B.ByteString, [ParsedPacketChunk])-drainParsedPackets !offset !idx !buffer acc- | B.null buffer = (offset, idx, B.empty, acc)- | otherwise =- case runGetOrFail getPkt (BL.fromStrict buffer) of- Left _ -> (offset, idx, buffer, acc)- Right (rest, consumed, pkt) ->- let !consumedLen = fromIntegral consumed- !nextOffset = offset + consumed- !nextIdx = idx + 1- !nextBuffer = BL.toStrict rest- parsedPacket =- ParsedPacketChunk- { ppcRange = ByteRange offset consumed- , ppcRaw = BL.fromStrict (B.take consumedLen buffer)- , ppcIndex = idx- , ppcValue = pkt- }- in drainParsedPackets- nextOffset- nextIdx- nextBuffer- (parsedPacket : acc)+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}++module Codec.Encryption.OpenPGP.Serialize+ ( -- * Serialization functions+ putPkt+ , putPktEither+ , putSKAddendum+ , getSecretKey+ , putSKeyForPKPayload++ -- * Utilities+ , dearmorIfAsciiArmored+ , dearmorIfAsciiArmoredLenient+ , looksLikeAsciiArmor+ , armorPayloadsOfType+ , singleArmorPayloadOfType+ , singleClearSignedBlock+ , recommendedArmorType+ , WireRepInput (..)+ , wireRepRefFromInput+ , PktParseError (..)+ , parsePkts+ , parsePktsEither+ , parsePktsWithWireRep+ , conduitParsePktsWithWireRep+ ) where++import qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA+import Codec.Encryption.OpenPGP.ASCIIArmor.Types+ ( Armor (..)+ , ArmorType (..)+ )+import Control.Applicative (many, some)+import Control.Arrow ((***))+import Control.Lens ((^.), _1)+import Control.Monad (guard, replicateM, replicateM_, when)+import Crypto.Number.Basic (numBits)+import Crypto.Number.ModArithmetic (inverse)+import Crypto.Number.Serialize (i2osp, os2ip)+import qualified Crypto.PubKey.DSA as D+import qualified Crypto.PubKey.ECC.ECDSA as ECDSA+import qualified Crypto.PubKey.ECC.Types as ECCT+import qualified Crypto.PubKey.RSA as R+import Data.Bifunctor (bimap)+import Data.Binary (Binary, get, put)+import Data.Binary.Get+ ( ByteOffset+ , Get+ , bytesRead+ , getByteString+ , getLazyByteString+ , getRemainingLazyByteString+ , getWord16be+ , getWord16le+ , getWord32be+ , getWord8+ , lookAhead+ , runGetOrFail+ )+import Data.Binary.Put+ ( Put+ , putByteString+ , putLazyByteString+ , putWord16be+ , putWord16le+ , putWord32be+ , putWord8+ , runPut+ )+import Data.Bits (shiftL, shiftR, testBit, (.&.), (.|.))+import qualified Data.ByteString as B+import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Lazy.Char8 as BLC8+import Data.Conduit (ConduitT, await, yield)+import qualified Data.Foldable as F+import Data.Int (Int64)+import Data.List (mapAccumL)+import qualified Data.List.NonEmpty as NE+import Data.Maybe (fromMaybe)+import Data.Set (Set)+import qualified Data.Set as Set+import qualified Data.Text as T+import Data.Text.Encoding (decodeUtf8With, encodeUtf8)+import Data.Text.Encoding.Error (lenientDecode)+import Data.Word (Word16, Word32, Word8)+import Network.URI (nullURI, parseURI, uriToString)++import Codec.Encryption.OpenPGP.Internal+ ( curve2Curve+ , curveFromCurve+ , curveToCurveoidBS+ , curveoidBSToCurve+ , curveoidBSToEdSigningCurve+ , edSigningCurveToCurveoidBS+ , leftPadTo+ , pubkeyToMPIs+ )+import Codec.Encryption.OpenPGP.Policy+ ( signatureV6SaltSizeForHashAlgorithm+ )+import Codec.Encryption.OpenPGP.Types+import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as BTypes+import qualified Codec.Encryption.OpenPGP.Types.Internal.PKITypes as P++instance Binary SigSubPacket where+ get = getSigSubPacket+ put = putSigSubPacket++-- instance Binary (Set NotationFlag) where+-- put = putNotationFlagSet+instance Binary CompressionAlgorithm where+ get = toFVal <$> getWord8+ put = putWord8 . fromFVal++instance Binary PubKeyAlgorithm where+ get = toFVal <$> getWord8+ put = putWord8 . fromFVal++instance Binary HashAlgorithm where+ get = toFVal <$> getWord8+ put = putWord8 . fromFVal++instance Binary SymmetricAlgorithm where+ get = toFVal <$> getWord8+ put = putWord8 . fromFVal++instance Binary AEADAlgorithm where+ get = toFVal <$> getWord8+ put = putWord8 . fromFVal++instance Binary MPI where+ get = getMPI+ put = putMPI++instance Binary SigType where+ get = toFVal <$> getWord8+ put = putWord8 . fromFVal++instance Binary UserAttrSubPacket where+ get = getUserAttrSubPacket+ put = putUserAttrSubPacket++instance Binary S2K where+ get = getS2K+ put = putS2K++instance Binary (PKESK 'PKESKV3) where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary (PKESK 'PKESKV6) where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary Signature where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary (SKESK 'SKESKV4) where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary (SKESK 'SKESKV6) where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary (OnePassSignature 'OPSV3) where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary (OnePassSignature 'OPSV6) where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary SecretKey where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary PublicKey where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary SecretSubkey where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary CompressedData where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary SymEncData where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary Marker where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary LiteralData where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary Trust where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary UserId where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary PublicSubkey where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary UserAttribute where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary SymEncIntegrityProtectedData where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary ModificationDetectionCode where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary OtherPacket where+ get = getPkt >>= either fail pure . fromPktEither+ put = putPkt . toPkt++instance Binary Pkt where+ get = getPkt+ put = putPkt++instance (Binary a) => Binary (Block a) where+ get = Block `fmap` many get+ put = mapM_ put . unBlock++instance Binary SomePKPayload where+ get = getPKPayload+ put = putPKPayload++instance Binary SignaturePayload where+ get = getSignaturePayload+ put = putSignaturePayload++instance Binary TKUnknown where+ get = fail "Binary TKUnknown decode is not implemented"+ put = putTK++getSigSubPacket :: Get SigSubPacket+getSigSubPacket = do+ l <- fmap fromIntegral getSubPacketLength+ (crit, pt) <- getSigSubPacketType+ getSigSubPacket' pt crit l+ where+ getSigSubPacket'+ :: Word8 -> Bool -> ByteOffset -> Get SigSubPacket+ getSigSubPacket' pt crit l+ | pt == 2 = do+ et <- fmap ThirtyTwoBitTimeStamp getWord32be+ return $ SigSubPacket crit (SigCreationTime et)+ | pt == 3 = do+ et <- fmap ThirtyTwoBitDuration getWord32be+ return $ SigSubPacket crit (SigExpirationTime et)+ | pt == 4 = do+ e <- get+ return $ SigSubPacket crit (ExportableCertification e)+ | pt == 5 = do+ tl <- getWord8+ ta <- getWord8+ return $ SigSubPacket crit (TrustSignature tl ta)+ | pt == 6 = do+ apdre <- getLazyByteString (l - 2)+ nul <- getWord8+ guard (nul == 0)+ return $ SigSubPacket crit (RegularExpression (BL.copy apdre))+ | pt == 7 = do+ r <- get+ return $ SigSubPacket crit (Revocable r)+ | pt == 9 = do+ et <- fmap ThirtyTwoBitDuration getWord32be+ return $ SigSubPacket crit (KeyExpirationTime et)+ | pt == 11 = do+ sa <- replicateM (fromIntegral (l - 1)) get+ return $ SigSubPacket crit (PreferredSymmetricAlgorithms sa)+ | pt == 12 = do+ rclass <- getWord8+ guard (testBit rclass 7)+ algid <- get+ fp <- getLazyByteString (fromIntegral l - 3)+ return $+ SigSubPacket+ crit+ ( RevocationKey+ (bsToFFSet . BL.singleton $ rclass .&. 0x7f)+ algid+ (Fingerprint fp)+ )+ | pt == 16 = do+ keyid <- getLazyByteString (l - 1)+ return $ SigSubPacket crit (Issuer (EightOctetKeyId keyid))+ | pt == 20 = do+ flags <- getLazyByteString 4+ nl <- getWord16be+ vl <- getWord16be+ nn <- getLazyByteString (fromIntegral nl)+ nv <- getLazyByteString (fromIntegral vl)+ return $+ SigSubPacket+ crit+ ( NotationData+ (bsToFFSet flags)+ (NotationName nn)+ (NotationValue nv)+ )+ | pt == 21 = do+ ha <- replicateM (fromIntegral (l - 1)) get+ return $ SigSubPacket crit (PreferredHashAlgorithms ha)+ | pt == 22 = do+ ca <- replicateM (fromIntegral (l - 1)) get+ return $ SigSubPacket crit (PreferredCompressionAlgorithms ca)+ | pt == 23 = do+ ksps <- getLazyByteString (l - 1)+ return $+ SigSubPacket crit (KeyServerPreferences (bsToFFSet ksps))+ | pt == 24 = do+ pks <- getLazyByteString (l - 1)+ return $ SigSubPacket crit (PreferredKeyServer pks)+ | pt == 25 = do+ primacy <- get+ return $ SigSubPacket crit (PrimaryUserId primacy)+ | pt == 26 = do+ url <-+ fmap+ ( URL+ . fromMaybe nullURI+ . parseURI+ . T.unpack+ . decodeUtf8With lenientDecode+ )+ (getByteString (fromIntegral (l - 1)))+ return $ SigSubPacket crit (PolicyURL url)+ | pt == 27 = do+ kfs <- getLazyByteString (l - 1)+ return $ SigSubPacket crit (KeyFlags (bsToFFSet kfs))+ | pt == 28 = do+ uid <- getByteString (fromIntegral (l - 1))+ return $+ SigSubPacket+ crit+ (SignersUserId (decodeUtf8With lenientDecode uid))+ | pt == 29 = do+ rcode <- getWord8+ rreason <-+ fmap+ (decodeUtf8With lenientDecode)+ (getByteString (fromIntegral (l - 2)))+ return $+ SigSubPacket crit (ReasonForRevocation (toFVal rcode) rreason)+ | pt == 30 = do+ fbs <- getLazyByteString (l - 1)+ return $ SigSubPacket crit (Features (bsToFFSet fbs))+ | pt == 31 = do+ pka <- get+ ha <- get+ hash <- getLazyByteString (l - 3)+ return $ SigSubPacket crit (SignatureTarget pka ha hash)+ | pt == 32 = do+ spbs <- getLazyByteString (l - 1)+ case runGetOrFail get spbs of+ Left (_, _, e) -> fail ("embedded signature subpacket " ++ e)+ Right (_, _, sp) -> return $ SigSubPacket crit (EmbeddedSignature sp)+ | pt == 33 = do+ when (l /= 22 && l /= 34) $+ fail ("invalid issuer fingerprint subpacket length: " ++ show l)+ kv <- getWord8+ let fpLen = l - 2+ when (fpLen /= 20 && fpLen /= 32) $+ fail ("invalid issuer fingerprint length: " ++ show fpLen)+ case BTypes.packetVersionToIssuerFingerprintVersion kv of+ Nothing ->+ fail ("invalid issuer fingerprint version marker: " ++ show kv)+ Just ifVersion -> do+ fp <-+ case kv of+ 4 -> getLazyByteString (fromIntegral fpLen)+ 6 -> getLazyByteString (fromIntegral fpLen)+ _ ->+ fail ("invalid issuer fingerprint version marker: " ++ show kv)+ return $+ SigSubPacket crit (IssuerFingerprint ifVersion (Fingerprint fp))+ | pt == 35 = do+ kv <- getWord8+ fp <- getLazyByteString (l - 2)+ when (BL.length fp /= 20 && BL.length fp /= 32) $+ fail+ ( "invalid intended recipient fingerprint length: "+ ++ show (BL.length fp)+ )+ case BTypes.packetVersionToIssuerFingerprintVersion kv of+ Nothing ->+ fail+ ( "invalid intended recipient fingerprint version marker: "+ ++ show kv+ )+ Just ifVersion ->+ return $+ SigSubPacket crit (IntendedRecipient ifVersion (Fingerprint fp))+ | pt == 39 = do+ let payloadLen = fromIntegral (l - 1)+ when (payloadLen `mod` 2 /= 0) $+ fail "preferred AEAD ciphersuites subpacket length must be even"+ pairs <- replicateM (payloadLen `div` 2) $ do+ sa <- get+ aead <- get+ return (sa, aead)+ return $ SigSubPacket crit (PreferredAEADCiphersuites pairs)+ | pt > 99 && pt < 111 = do+ payload <- getLazyByteString (l - 1)+ return $ SigSubPacket crit (UserDefinedSigSub pt payload)+ | otherwise = do+ payload <- getLazyByteString (l - 1)+ return $ SigSubPacket crit (OtherSigSub pt payload)++putSigSubPacket :: SigSubPacket -> Put+putSigSubPacket (SigSubPacket crit (SigCreationTime et)) = do+ putSubPacketLength 5+ putSigSubPacketType crit 2+ putWord32be . unThirtyTwoBitTimeStamp $ et+putSigSubPacket (SigSubPacket crit (SigExpirationTime et)) = do+ putSubPacketLength 5+ putSigSubPacketType crit 3+ putWord32be . unThirtyTwoBitDuration $ et+putSigSubPacket (SigSubPacket crit (ExportableCertification e)) = do+ putSubPacketLength 2+ putSigSubPacketType crit 4+ put e+putSigSubPacket (SigSubPacket crit (TrustSignature tl ta)) = do+ putSubPacketLength 3+ putSigSubPacketType crit 5+ put tl+ put ta+putSigSubPacket (SigSubPacket crit (RegularExpression apdre)) = do+ putSubPacketLength . fromIntegral $ (2 + BL.length apdre)+ putSigSubPacketType crit 6+ putLazyByteString apdre+ putWord8 0+putSigSubPacket (SigSubPacket crit (Revocable r)) = do+ putSubPacketLength 2+ putSigSubPacketType crit 7+ put r+putSigSubPacket (SigSubPacket crit (KeyExpirationTime et)) = do+ putSubPacketLength 5+ putSigSubPacketType crit 9+ putWord32be . unThirtyTwoBitDuration $ et+putSigSubPacket (SigSubPacket crit (PreferredSymmetricAlgorithms ess)) = do+ putSubPacketLength . fromIntegral $ (1 + length ess)+ putSigSubPacketType crit 11+ mapM_ put ess+putSigSubPacket (SigSubPacket crit (RevocationKey rclass algid fp)) = do+ let fpLen = BL.length (unFingerprint fp)+ putSubPacketLength (fromIntegral (3 + fpLen)) -- type(1) + rclass(1) + algid(1) + fingerprint+ putSigSubPacketType crit 12+ putLazyByteString . ffSetToFixedLengthBS (1 :: Int) $+ Set.insert (RClOther 0) rclass+ put algid+ putLazyByteString (unFingerprint fp)+putSigSubPacket (SigSubPacket crit (Issuer keyid)) = do+ putSubPacketLength 9+ putSigSubPacketType crit 16+ putLazyByteString (unEOKI keyid) -- 8 octets+putSigSubPacket+ ( SigSubPacket+ crit+ (NotationData nfs (NotationName nn) (NotationValue nv))+ ) = do+ putSubPacketLength . fromIntegral $+ (9 + BL.length nn + BL.length nv)+ putSigSubPacketType crit 20+ putLazyByteString . ffSetToFixedLengthBS (4 :: Int) $ nfs+ putWord16be . fromIntegral . BL.length $ nn+ putWord16be . fromIntegral . BL.length $ nv+ putLazyByteString nn+ putLazyByteString nv+putSigSubPacket (SigSubPacket crit (PreferredHashAlgorithms ehs)) = do+ putSubPacketLength . fromIntegral $ (1 + length ehs)+ putSigSubPacketType crit 21+ mapM_ put ehs+putSigSubPacket (SigSubPacket crit (PreferredCompressionAlgorithms ecs)) = do+ putSubPacketLength . fromIntegral $ (1 + length ecs)+ putSigSubPacketType crit 22+ mapM_ put ecs+putSigSubPacket (SigSubPacket crit (KeyServerPreferences ksps)) = do+ let kbs = ffSetToBS ksps+ putSubPacketLength . fromIntegral $ (1 + BL.length kbs)+ putSigSubPacketType crit 23+ putLazyByteString kbs+putSigSubPacket (SigSubPacket crit (PreferredKeyServer ks)) = do+ putSubPacketLength . fromIntegral $ (1 + BL.length ks)+ putSigSubPacketType crit 24+ putLazyByteString ks+putSigSubPacket (SigSubPacket crit (PrimaryUserId primacy)) = do+ putSubPacketLength 2+ putSigSubPacketType crit 25+ put primacy+putSigSubPacket (SigSubPacket crit (PolicyURL (URL uri))) = do+ let bs = encodeUtf8 (T.pack (uriToString id uri ""))+ putSubPacketLength . fromIntegral $ (1 + B.length bs)+ putSigSubPacketType crit 26+ putByteString bs+putSigSubPacket (SigSubPacket crit (KeyFlags kfs)) = do+ let kbs = ffSetToBS kfs+ putSubPacketLength . fromIntegral $ (1 + BL.length kbs)+ putSigSubPacketType crit 27+ putLazyByteString kbs+putSigSubPacket (SigSubPacket crit (SignersUserId userid)) = do+ let bs = encodeUtf8 userid+ putSubPacketLength . fromIntegral $ (1 + B.length bs)+ putSigSubPacketType crit 28+ putByteString bs+putSigSubPacket (SigSubPacket crit (ReasonForRevocation rcode rreason)) = do+ let reasonbs = encodeUtf8 rreason+ putSubPacketLength . fromIntegral $ (2 + B.length reasonbs)+ putSigSubPacketType crit 29+ putWord8 . fromFVal $ rcode+ putByteString reasonbs+putSigSubPacket (SigSubPacket crit (Features fs)) = do+ let fbs = ffSetToBS fs+ putSubPacketLength . fromIntegral $ (1 + BL.length fbs)+ putSigSubPacketType crit 30+ putLazyByteString fbs+putSigSubPacket (SigSubPacket crit (SignatureTarget pka ha hash)) = do+ putSubPacketLength . fromIntegral $ (3 + BL.length hash)+ putSigSubPacketType crit 31+ put pka+ put ha+ putLazyByteString hash+putSigSubPacket (SigSubPacket crit (EmbeddedSignature sp)) = do+ let spb = runPut (put sp)+ putSubPacketLength . fromIntegral $ (1 + BL.length spb)+ putSigSubPacketType crit 32+ putLazyByteString spb+putSigSubPacket (SigSubPacket crit (IssuerFingerprint kv fp)) = do+ let kv' = BTypes.issuerFingerprintVersionToPacketVersion kv+ let fpb = unFingerprint fp+ when (BL.length fpb /= 20 && BL.length fpb /= 32) $+ error+ ("invalid issuer fingerprint length: " ++ show (BL.length fpb))+ putSubPacketLength . fromIntegral $ (2 + BL.length fpb)+ putSigSubPacketType crit 33+ putWord8 kv'+ putLazyByteString fpb+putSigSubPacket (SigSubPacket crit (IntendedRecipient kv irf)) = do+ let kv' = BTypes.issuerFingerprintVersionToPacketVersion kv+ let fpb = unFingerprint irf+ when (BL.length fpb /= 20 && BL.length fpb /= 32) $+ error+ ( "invalid intended-recipient fingerprint length: "+ ++ show (BL.length fpb)+ )+ putSubPacketLength . fromIntegral $ (2 + BL.length fpb)+ putSigSubPacketType crit 35+ putWord8 kv'+ putLazyByteString fpb+putSigSubPacket (SigSubPacket crit (PreferredAEADCiphersuites ps)) = do+ putSubPacketLength . fromIntegral $ (1 + 2 * length ps)+ putSigSubPacketType crit 39+ mapM_ (\(sa, aead) -> put sa >> put aead) ps+putSigSubPacket (SigSubPacket crit (UserDefinedSigSub ptype payload)) =+ putSigSubPacket (SigSubPacket crit (OtherSigSub ptype payload))+putSigSubPacket (SigSubPacket crit (OtherSigSub ptype payload)) = do+ putSubPacketLength . fromIntegral $ (1 + BL.length payload)+ putSigSubPacketType crit ptype+ putLazyByteString payload++getSubPacketLength :: Get Word32+getSubPacketLength = getSubPacketLength' =<< getWord8+ where+ getSubPacketLength' :: (Integral a) => Word8 -> Get a+ getSubPacketLength' f+ | f < 192 = return . fromIntegral $ f+ | f < 224 = do+ secondOctet <- getWord8+ return . fromIntegral $+ shiftL (fromIntegral (f - 192) :: Int) 8+ + (fromIntegral secondOctet :: Int)+ + 192+ | f == 255 = do+ len <- getWord32be+ return . fromIntegral $ len+ | otherwise = fail "Partial body length invalid."++putSubPacketLength :: Word32 -> Put+putSubPacketLength l+ | l < 192 = putWord8 (fromIntegral l)+ | l < 8384 =+ putWord8+ (fromIntegral ((fromIntegral (l - 192) `shiftR` 8) + 192 :: Int))+ >> putWord8 (fromIntegral (l - 192) .&. 0xff)+ | l <= 0xffffffff = putWord8 255 >> putWord32be (fromIntegral l)+ | otherwise = error ("too big (" ++ show l ++ ")")++getSigSubPacketType :: Get (Bool, Word8)+getSigSubPacketType = do+ x <- getWord8+ return+ ( if x .&. 128 == 128+ then (True, x .&. 127)+ else (False, x)+ )++putSigSubPacketType :: Bool -> Word8 -> Put+putSigSubPacketType False sst = putWord8 sst+putSigSubPacketType True sst = putWord8 (sst .|. 0x80)++bsToFFSet :: (FutureFlag a) => ByteString -> Set a+bsToFFSet bs =+ Set.fromAscList . concat . snd $+ mapAccumL+ (\acc y -> (acc + 8, concatMap (shifty acc y) [0 .. 7]))+ 0+ (BL.unpack bs)+ where+ shifty acc y x = [toFFlag (acc + x) | y .&. shiftR 128 x == shiftR 128 x]++ffSetToFixedLengthBS+ :: (FutureFlag b, Integral a) => a -> Set b -> ByteString+ffSetToFixedLengthBS len ffs =+ BL.take+ (fromIntegral len)+ (BL.append (ffSetToBS ffs) (BL.pack (replicate 5 0)))++ffSetToBS :: (FutureFlag a) => Set a -> ByteString+ffSetToBS = BL.pack . ffSetToBS'+ where+ ffSetToBS' :: (FutureFlag a) => Set a -> [Word8]+ ffSetToBS' ks+ -- Emit a single zero octet for an empty flag set so encoded flag+ -- subpackets always carry an explicit flags byte.+ | Set.null ks = [0]+ | otherwise =+ map+ ( ( foldl (.|.) 0+ . map (shiftR 128 . flip mod 8 . fromFFlag)+ . Set.toAscList+ )+ . (\x -> Set.filter (\y -> fromFFlag y `div` 8 == x) ks)+ )+ [0 .. fromFFlag (Set.findMax ks) `div` 8]++fromS2K :: S2K -> ByteString+fromS2K (Simple hashalgo) = BL.pack [0, fromIntegral . fromFVal $ hashalgo]+fromS2K (Salted hashalgo salt) =+ BL.pack [1, fromIntegral . fromFVal $ hashalgo]+ `BL.append` (BL.fromStrict . unSalt8) salt+fromS2K (IteratedSalted hashalgo salt count) =+ BL.pack [3, fromIntegral . fromFVal $ hashalgo]+ `BL.append` (BL.fromStrict . unSalt8) salt+ `BL.snoc` encodeIterationCount count+fromS2K (Argon2 salt t p encodedM) =+ BL.pack [4]+ `BL.append` (BL.fromStrict . unSalt16) salt+ `BL.append` BL.pack [t, p, encodedM]+fromS2K (OtherS2K _ bs) = bs++getPacketLength :: Get Integer+getPacketLength = do+ firstOctet <- getWord8+ lenOrPartial <- lengthOctetToLength firstOctet+ case lenOrPartial of+ Left _ ->+ fail "Partial body length is invalid in this context"+ Right len -> return len+ where+ lengthOctetToLength :: Word8 -> Get (Either Integer Integer)+ lengthOctetToLength f+ | f < 192 = return . Right . fromIntegral $ f+ | f < 224 = do+ secondOctet <- getWord8+ return . Right . fromIntegral $+ shiftL (fromIntegral (f - 192) :: Int) 8+ + (fromIntegral secondOctet :: Int)+ + 192+ | f < 255 =+ return . Left . fromIntegral $+ (1 :: Integer) `shiftL` fromIntegral (f .&. 0x1f)+ | otherwise = do+ len <- getWord32be+ return . Right . fromIntegral $ len++putPacketLength :: Integer -> Put+putPacketLength l+ | l < 192 = putWord8 (fromIntegral l)+ | l < 8384 =+ putWord8+ (fromIntegral ((fromIntegral (l - 192) `shiftR` 8) + 192 :: Int))+ >> putWord8 (fromIntegral (l - 192) .&. 0xff)+ | l < 0x100000000 = putWord8 255 >> putWord32be (fromIntegral l)+ | otherwise =+ error "packet length exceeds 32-bit definite length encoding"++putPartialLength :: Word8 -> Put+putPartialLength n = putWord8 (224 + n)++getPacketLengthFromOctet :: Word8 -> Get (Either Int64 Int64)+getPacketLengthFromOctet f+ | f < 192 = return . Right . fromIntegral $ f+ | f < 224 = do+ secondOctet <- getWord8+ return . Right . fromIntegral $+ shiftL (fromIntegral (f - 192) :: Int) 8+ + (fromIntegral secondOctet :: Int)+ + 192+ | f < 255 =+ return . Left . fromIntegral $+ (1 :: Integer) `shiftL` fromIntegral (f .&. 0x1f)+ | otherwise = do+ len <- getWord32be+ return . Right . fromIntegral $ len++getS2K :: Get S2K+getS2K = getS2K' =<< getWord8+ where+ getS2K' :: Word8 -> Get S2K+ getS2K' t+ | t == 0 = do+ ha <- getWord8+ return $ Simple (toFVal ha)+ | t == 1 = do+ ha <- getWord8+ salt <- getByteString 8+ return $ Salted (toFVal ha) (Salt8 salt)+ | t == 3 = do+ ha <- getWord8+ salt <- getByteString 8+ count <- getWord8+ return $+ IteratedSalted+ (toFVal ha)+ (Salt8 salt)+ (decodeIterationCount count)+ | t == 4 = do+ salt <- getByteString 16+ passes <- getWord8+ parallelism <- getWord8+ encodedM <- getWord8+ return $ Argon2 (Salt16 salt) passes parallelism encodedM+ | otherwise = do+ bs <- getRemainingLazyByteString+ return $ OtherS2K t bs++putS2K :: S2K -> Put+putS2K (Simple hashalgo) = error ("confused by simple" ++ show hashalgo)+putS2K (Salted hashalgo salt) =+ error+ ("confused by salted" ++ show hashalgo ++ " by " ++ show salt)+putS2K (IteratedSalted ha salt count) = do+ putWord8 3+ put ha+ putByteString (unSalt8 salt)+ putWord8 $ encodeIterationCount count+putS2K (Argon2 salt t p encodedM) = do+ putWord8 4+ putByteString (unSalt16 salt)+ putWord8 t+ putWord8 p+ putWord8 encodedM+putS2K (OtherS2K t bs) = putWord8 t >> putLazyByteString bs++v6SaltSizeForHashAlgorithm :: HashAlgorithm -> Maybe Word8+v6SaltSizeForHashAlgorithm = signatureV6SaltSizeForHashAlgorithm++getPacketTypeAndPayload :: Get (Word8, ByteString)+getPacketTypeAndPayload = do+ tag <- getWord8+ guard (testBit tag 7)+ case tag .&. 0x40 of+ 0x00 -> do+ let t = shiftR (tag .&. 0x3c) 2+ case tag .&. 0x03 of+ 0 -> do+ len <- getWord8+ bs <- getLazyByteString (fromIntegral len)+ return (t, bs)+ 1 -> do+ len <- getWord16be+ bs <- getLazyByteString (fromIntegral len)+ return (t, bs)+ 2 -> do+ len <- getWord32be+ bs <- getLazyByteString (fromIntegral len)+ return (t, bs)+ 3 -> do+ bs <- getRemainingLazyByteString+ return (t, bs)+ _ ->+ error "This should never happen (getPacketTypeAndPayload/0x00)."+ 0x40 -> do+ firstLenOctet <- getWord8+ bs <- getPacketPayloadFromLengthOctet firstLenOctet+ return (tag .&. 0x3f, bs)+ _ ->+ error "This should never happen (getPacketTypeAndPayload/???)."+ where+ getPacketPayloadFromLengthOctet :: Word8 -> Get ByteString+ getPacketPayloadFromLengthOctet lenOctet = do+ lenOrPartial <- getPacketLengthFromOctet lenOctet+ case lenOrPartial of+ Right len -> getLazyByteString len+ Left partialLen -> do+ chunk <- getLazyByteString partialLen+ rest <- getRemainingPartialPayload+ return (chunk <> rest)+ getRemainingPartialPayload :: Get ByteString+ getRemainingPartialPayload = do+ lenOctet <- getWord8+ lenOrPartial <- getPacketLengthFromOctet lenOctet+ case lenOrPartial of+ Right len -> getLazyByteString len+ Left partialLen -> do+ chunk <- getLazyByteString partialLen+ (chunk <>) <$> getRemainingPartialPayload++getPkt :: Get Pkt+getPkt = do+ (t, pl) <- getPacketTypeAndPayload+ case runGetOrFail (getPkt' t (BL.length pl)) pl of+ Left (_, _, e) -> return $! BrokenPacketPkt e t pl+ Right (_, _, p) -> return p+ where+ parseLegacyPKESK+ :: PacketVersion -> BL.ByteString -> Either String Pkt+ parseLegacyPKESK pv body = do+ (_, _, (eokeyid, pkaRaw, mpib)) <-+ bimap (\(_, _, e) -> e) id $+ runGetOrFail+ ( do+ eokeyid <- getLazyByteString 8+ pka <- getWord8+ mpib <- getRemainingLazyByteString+ pure (eokeyid, pka, mpib)+ )+ body+ let pka = toFVal pkaRaw+ sk <- parseLegacyPKESKMPIs pka mpib+ pure $+ PKESKPkt+ ( PKESKPayloadV3Packet+ (PKESKPayloadV3 pv (EightOctetKeyId eokeyid) pka sk)+ )++ parseLegacyPKESKMPIs+ :: PubKeyAlgorithm+ -> BL.ByteString+ -> Either String (NE.NonEmpty MPI)+ parseLegacyPKESKMPIs pka mpib = do+ case parseLegacyPKESKMPIsStrict pka mpib of+ Right sk -> pure sk+ Left strictErr+ | pka == X25519 ->+ case parseLegacyPKESKX25519V3Octets mpib of+ Right sk -> Right sk+ Left octetErr ->+ Left+ ( strictErr+ ++ "; also failed to parse RFC9580 X25519 v3 octet layout: "+ ++ octetErr+ )+ | pka == ECDH ->+ case parseLegacyPKESKECDHOctets mpib of+ Right sk -> Right sk+ Left octetErr ->+ Left+ ( strictErr+ ++ "; also failed to parse RFC6637 ECDH v3 octet layout: "+ ++ octetErr+ )+ | otherwise -> Left strictErr++ parseLegacyPKESKMPIsStrict+ :: PubKeyAlgorithm+ -> BL.ByteString+ -> Either String (NE.NonEmpty MPI)+ parseLegacyPKESKMPIsStrict pka mpib = do+ (rest, _, sk) <-+ bimap (\(_, _, e) -> e) id $+ runGetOrFail (parserForLegacyPKESKMPIs pka) mpib+ if BL.null rest+ then pure (NE.fromList sk)+ else+ Left+ ("unexpected trailing PKESK MPI data for algorithm " ++ show pka)++ parseLegacyPKESKX25519V3Octets+ :: BL.ByteString -> Either String (NE.NonEmpty MPI)+ parseLegacyPKESKX25519V3Octets mpib = do+ if BL.length mpib < 33+ then Left "X25519 v3 PKESK octet layout is too short"+ else Right ()+ let ephemeral = BL.toStrict (BL.take 32 mpib)+ eskLen = fromIntegral (BL.index mpib 32) :: Int+ eskWithAlgo = BL.toStrict (BL.drop 33 mpib)+ if eskLen /= B.length eskWithAlgo+ then+ Left "X25519 v3 PKESK octet layout has inconsistent ESK length"+ else Right ()+ if B.null eskWithAlgo+ then+ Left+ "X25519 v3 PKESK octet layout must include a symmetric algorithm octet"+ else Right ()+ let symAlgo = B.head eskWithAlgo+ if symAlgo+ `elem` [ fromIntegral (fromFVal AES128)+ , fromIntegral (fromFVal AES192)+ , fromIntegral (fromFVal AES256)+ ]+ then+ pure+ (NE.fromList [MPI (os2ip ephemeral), MPI (os2ip eskWithAlgo)])+ else+ Left+ ( "X25519 v3 PKESK octet layout has unsupported symmetric algorithm octet "+ ++ show symAlgo+ )++ -- \| Parse an RFC 6637 §8 ECDH PKESKv3 body as MPI(ephemeral) || 1-octet-count || C.+ -- This is the interoperable wire format produced by GnuPG and other RFC-compliant+ -- implementations. hOpenPGP previously wrote both fields as MPIs; this fallback+ -- allows reading RFC-compliant packets when the strict two-MPI path fails.+ parseLegacyPKESKECDHOctets+ :: BL.ByteString -> Either String (NE.NonEmpty MPI)+ parseLegacyPKESKECDHOctets mpib = do+ (rest, _, ephMPI) <-+ bimap (\(_, _, e) -> e) id $ runGetOrFail getMPI mpib+ let restBS = BL.toStrict rest+ when (B.null restBS) $+ Left+ "ECDH v3 PKESK RFC6637 octet layout: missing wrapped-key length octet after ephemeral MPI"+ let wrappedLen = fromIntegral (B.head restBS) :: Int+ wrapped = B.tail restBS+ when (wrappedLen /= B.length wrapped) $+ Left+ ( "ECDH v3 PKESK RFC6637 octet layout: wrapped key length field "+ ++ show wrappedLen+ ++ " does not match body length "+ ++ show (B.length wrapped)+ )+ when (wrappedLen < 24 || wrappedLen `mod` 8 /= 0) $+ Left+ ( "ECDH v3 PKESK RFC6637 octet layout: wrapped key length "+ ++ show wrappedLen+ ++ " is not a valid RFC 3394 wrapped key size"+ )+ pure (ephMPI NE.:| [MPI (os2ip wrapped)])++ parserForLegacyPKESKMPIs :: PubKeyAlgorithm -> Get [MPI]+ parserForLegacyPKESKMPIs pka =+ case expectedLegacyPKESKMPIArity pka of+ Just mpiCount -> replicateM mpiCount getMPI+ Nothing -> some getMPI++ expectedLegacyPKESKMPIArity :: PubKeyAlgorithm -> Maybe Int+ expectedLegacyPKESKMPIArity pka+ | pka `elem` [RSA, DeprecatedRSAEncryptOnly] = Just 1+ | pka `elem` [ElgamalEncryptOnly, ForbiddenElgamal, ECDH, X25519] =+ Just 2+ | otherwise = Nothing++ validateV4SKESKEncryptedSessionKeyS2K+ :: S2K -> Maybe BL.ByteString -> Get ()+ validateV4SKESKEncryptedSessionKeyS2K _ Nothing = pure ()+ validateV4SKESKEncryptedSessionKeyS2K Simple {} (Just _) =+ fail+ "v4 SKESK packets with encrypted session keys must not use Simple S2K"+ validateV4SKESKEncryptedSessionKeyS2K _ (Just _) = pure ()++ parseV6PKESK :: BL.ByteString -> Either String Pkt+ parseV6PKESK body = do+ (_, _, (recipientKeyIdentifier, pka, esk)) <-+ bimap (\(_, _, e) -> e) id $+ runGetOrFail+ ( do+ keyIdentifierLen <- getWord8+ recipientKeyIdentifier <-+ getLazyByteString (fromIntegral keyIdentifierLen)+ pka <- getWord8+ esk <- getRemainingLazyByteString+ pure (recipientKeyIdentifier, pka, esk)+ )+ body+ validateV6PKESKRecipientIdentifier recipientKeyIdentifier+ pure $+ PKESKPkt+ ( PKESKPayloadV6Packet+ (PKESKPayloadV6 recipientKeyIdentifier (toFVal pka) esk)+ )+ where+ validateV6PKESKRecipientIdentifier+ :: BL.ByteString -> Either String ()+ validateV6PKESKRecipientIdentifier rid =+ case BL.length rid of+ 0 -> Right ()+ 20 -> Right ()+ 32 -> Right ()+ 21 -> validateVersionedFingerprint rid+ 33 -> validateVersionedFingerprint rid+ ridLen ->+ Left+ ( "invalid PKESK v6 recipient identifier length: "+ ++ show ridLen+ ++ " (expected 0, 20, 21, 32, or 33)"+ )++ validateVersionedFingerprint :: BL.ByteString -> Either String ()+ validateVersionedFingerprint rid =+ let keyVersion = BL.head rid+ fingerprintLen = BL.length (BL.tail rid)+ in case keyVersion of+ 4 ->+ if fingerprintLen == 20+ then Right ()+ else+ Left+ ( "PKESK v6 recipient identifier length/version mismatch: key version 4 requires fingerprint length 20, got "+ ++ show fingerprintLen+ )+ 6 ->+ if fingerprintLen == 32+ then Right ()+ else+ Left+ ( "PKESK v6 recipient identifier length/version mismatch: key version 6 requires fingerprint length 32, got "+ ++ show fingerprintLen+ )+ _ ->+ Left+ ( "invalid PKESK v6 recipient key version: "+ ++ show keyVersion+ ++ " (expected 4 or 6)"+ )++ getPkt' :: Word8 -> ByteOffset -> Get Pkt+ getPkt' t len+ | t == 1 = do+ pv <- getWord8+ body <- getRemainingLazyByteString+ if pv == 6+ then case parseV6PKESK body of+ Right pkt -> return pkt+ Left v6Err -> fail ("PKESK v6 parse failed: " ++ v6Err)+ else case parseLegacyPKESK pv body of+ Right pkt -> return pkt+ Left legacyErr -> fail ("PKESK MPIs " ++ legacyErr)+ | t == 2 = do+ bs <- getRemainingLazyByteString+ case runGetOrFail get bs of+ Left (_, _, e) -> fail ("signature packet " ++ e)+ Right (_, _, sp) -> return $ SignaturePkt sp+ | t == 3 = do+ pv <- getWord8+ let getV6SKESKParams = do+ symalgoWord <- getWord8+ aeadWord <- getWord8+ s2kLen <- getWord8+ s2kBytes <- getLazyByteString (fromIntegral s2kLen)+ s2k <-+ case runGetOrFail getS2K s2kBytes of+ Left (_, _, err) -> fail err+ Right (rest, _, parsed)+ | not (BL.null rest) ->+ fail "unexpected trailing bytes in v6 SKESK S2K specifier"+ | otherwise -> pure parsed+ let symalgo = toFVal symalgoWord+ aead = toFVal aeadWord+ ivLen = fromIntegral (aeadNonceSize aead)+ iv <- getLazyByteString ivLen+ pure (symalgo, aead, s2k, iv)+ case pv of+ 6 -> do+ paramsLen <- getWord8+ params <- getLazyByteString (fromIntegral paramsLen)+ (symalgo, aead, s2k, iv) <-+ case runGetOrFail getV6SKESKParams params of+ Left (_, _, err) -> fail err+ Right (rest, _, parsed)+ | not (BL.null rest) ->+ fail "unexpected trailing v6 SKESK parameters"+ | otherwise -> pure parsed+ payload <- getRemainingLazyByteString+ when (BL.length payload < 16) $+ fail+ "v6 SKESK payload must include encrypted session key and authentication tag"+ let (esk, tag) = BL.splitAt (BL.length payload - 16) payload+ return $+ SKESKPkt+ ( SKESKPayloadV6Packet+ ( SKESKPayloadV6+ symalgo+ aead+ s2k+ iv+ esk+ tag+ )+ )+ 4 -> do+ symalgo <- getWord8+ s2k <- getS2K+ esk <- getRemainingLazyByteString+ let mesk = if BL.null esk then Nothing else Just esk+ validateV4SKESKEncryptedSessionKeyS2K s2k mesk+ return $+ SKESKPkt+ ( SKESKPayloadV4Packet+ ( SKESKPayloadV4+ (toFVal symalgo)+ s2k+ mesk+ )+ )+ _ -> fail ("unsupported SKESK packet version " ++ show pv)+ | t == 4 = do+ pv <- getWord8+ sigtype <- toFVal <$> getWord8+ ha <- toFVal <$> getWord8+ pka <- toFVal <$> getWord8+ case pv of+ 3 -> do+ skeyid <- getLazyByteString 8+ nested <- getWord8 >>= parseOPSNestedFlag+ return $+ OnePassSignaturePkt+ ( OPSPayloadV3Packet+ ( OPSPayloadV3+ pv+ sigtype+ ha+ pka+ (EightOctetKeyId skeyid)+ nested+ )+ )+ 6 -> do+ saltSize <- getWord8+ expectedSaltSize <-+ maybe+ ( fail+ ( "signature hash algorithm does not define a V6 salt size: "+ ++ show ha+ )+ )+ pure+ (v6SaltSizeForHashAlgorithm ha)+ when (saltSize /= expectedSaltSize) $+ fail+ ( "OPS v6 salt size mismatch for "+ ++ show ha+ ++ ": expected "+ ++ show expectedSaltSize+ ++ ", got "+ ++ show saltSize+ )+ salt <-+ SignatureSalt <$> getLazyByteString (fromIntegral saltSize)+ signerFingerprint <- getLazyByteString 32+ nested <- getWord8 >>= parseOPSNestedFlag+ return $+ OnePassSignaturePkt+ ( OPSPayloadV6Packet+ ( OPSPayloadV6+ sigtype+ ha+ pka+ salt+ signerFingerprint+ nested+ )+ )+ _ -> fail ("Unsupported OPS version: " ++ show pv)+ | t == 5 = do+ bs <- getLazyByteString len+ let ps =+ flip runGetOrFail bs $ do+ pkp <- getPKPayload+ ska <- getSKAddendum pkp+ return $ SecretKeyPkt pkp ska+ case ps of+ Left (_, _, err) -> fail ("secret key " ++ err)+ Right (_, _, pkt) -> return pkt+ | t == 6 = do+ pkp <- getPKPayload+ return $ PublicKeyPkt pkp+ | t == 7 = do+ bs <- getLazyByteString len+ let ps =+ flip runGetOrFail bs $ do+ pkp <- getPKPayload+ ska <- getSKAddendum pkp+ return $ SecretSubkeyPkt pkp ska+ case ps of+ Left (_, _, err) -> fail ("secret subkey " ++ err)+ Right (_, _, pkt) -> return pkt+ | t == 8 = do+ ca <- getWord8+ cdata <- getLazyByteString (len - 1)+ return $ CompressedDataPkt (toFVal ca) cdata+ | t == 9 = do+ sdata <- getLazyByteString len+ return $ SymEncDataPkt sdata+ | t == 10 = do+ marker <- getLazyByteString len+ return $ MarkerPkt marker+ | t == 11 = do+ dt <- getWord8+ flen <- getWord8+ fn <- getLazyByteString (fromIntegral flen)+ ts <- fmap ThirtyTwoBitTimeStamp getWord32be+ ldata <- getLazyByteString (len - (6 + fromIntegral flen))+ return $ LiteralDataPkt (toFVal dt) fn ts ldata+ | t == 12 = do+ tdata <- getLazyByteString len+ return $ TrustPkt tdata+ | t == 13 = do+ udata <- getByteString (fromIntegral len)+ return . UserIdPkt . decodeUtf8With lenientDecode $ udata+ | t == 14 = do+ bs <- getLazyByteString len+ let ps =+ flip runGetOrFail bs $ do+ pkp <- getPKPayload+ return $ PublicSubkeyPkt pkp+ case ps of+ Left (_, _, err) -> fail ("public subkey " ++ err)+ Right (_, _, pkt) -> return pkt+ | t == 17 = do+ bs <- getLazyByteString len+ case runGetOrFail (many getUserAttrSubPacket) bs of+ Left (_, _, err) -> fail ("user attribute " ++ err)+ Right (_, _, uas) -> return $ UserAttributePkt uas+ | t == 18 = do+ pv <- getWord8+ case pv of+ 1 -> do+ b <- getLazyByteString (len - 1)+ return $ SymEncIntegrityProtectedDataPkt (SEIPD1 pv b)+ 2 -> do+ when (len < 36) $+ fail "SEIPD v2 packet too short"+ symalgo <- toFVal <$> getWord8+ aeadalgo <- toFVal <$> getWord8+ chunkSize <- getWord8+ salt <- Salt <$> getByteString 32+ encrypted <- getLazyByteString (len - 36)+ validateSEIPDv2Header symalgo aeadalgo chunkSize encrypted+ return $+ SymEncIntegrityProtectedDataPkt+ ( SEIPD2+ symalgo+ aeadalgo+ chunkSize+ salt+ encrypted+ )+ _ -> fail ("Unsupported SEIPD version: " ++ show pv)+ | t == 19 = do+ hash <- getLazyByteString 20+ return $ ModificationDetectionCodePkt hash+ | t == 21 = do+ payload <- getLazyByteString len+ return $ PaddingPkt payload+ | otherwise = do+ payload <- getLazyByteString len+ return $ OtherPacketPkt t payload++getUserAttrSubPacket :: Get UserAttrSubPacket+getUserAttrSubPacket = do+ l <- fmap fromIntegral getSubPacketLength+ t <- getWord8+ getUserAttrSubPacket' t l+ where+ getUserAttrSubPacket'+ :: Word8 -> ByteOffset -> Get UserAttrSubPacket+ getUserAttrSubPacket' t l+ | t == 1 = do+ _ <- getWord16le -- ihlen+ hver <- getWord8 -- should be 1+ iformat <- getWord8+ nuls <- getLazyByteString 12 -- should be NULs+ bs <- getLazyByteString (l - 17)+ if hver /= 1 || nuls /= BL.pack (replicate 12 0)+ then fail "Corrupt UAt subpacket"+ else return $ ImageAttribute (ImageHV1 (toFVal iformat)) bs+ | otherwise = do+ bs <- getLazyByteString (l - 1)+ return $ OtherUASub t bs++putUserAttrSubPacket :: UserAttrSubPacket -> Put+putUserAttrSubPacket ua = do+ let sp = runPut $ putUserAttrSubPacket' ua+ putSubPacketLength . fromIntegral . BL.length $ sp+ putLazyByteString sp+ where+ putUserAttrSubPacket' (ImageAttribute (ImageHV1 iformat) idata) = do+ putWord8 1+ putWord16le 16+ putWord8 1+ putWord8 (fromFVal iformat)+ replicateM_ 12 $ putWord8 0+ putLazyByteString idata+ putUserAttrSubPacket' (OtherUASub t bs) = do+ putWord8 t+ putLazyByteString bs++{- | Serialize PKESKv3 session-key material.+For ECDH and X25519 the RFC 6637 §8 / RFC 9580 §5.1.6 wire format is used:+MPI(ephemeral_key) || 1-octet-count || wrapped_session_key_bytes.+All other algorithms use the standard MPI sequence.+-}+putPKESKv3SessionKeyMaterial+ :: PubKeyAlgorithm -> NE.NonEmpty MPI -> Put+putPKESKv3SessionKeyMaterial pka mpis+ | pka `elem` [ECDH, X25519]+ , (ephMPI NE.:| [wrappedMPI]) <- mpis = do+ put ephMPI+ let rawWrapped = i2osp (unMPI wrappedMPI)+ -- Left-pad to the nearest valid RFC 3394 wrapped-key length so that+ -- leading-zero bytes stripped by i2osp are restored.+ targetLen =+ headDef+ (B.length rawWrapped)+ (filter (>= B.length rawWrapped) [32, 40, 48])+ paddedWrapped = leftPadTo targetLen rawWrapped+ putWord8 (fromIntegral (B.length paddedWrapped))+ putByteString paddedWrapped+ | otherwise = F.mapM_ put mpis+ where+ headDef d [] = d+ headDef _ (x : _) = x++putPkt :: Pkt -> Put+putPkt+ ( PKESKPkt+ (PKESKPayloadV3Packet (PKESKPayloadV3 pv eokeyid pka mpis))+ ) = do+ putWord8 (0xc0 .|. 1)+ let bsk = runPut $ putPKESKv3SessionKeyMaterial pka mpis+ putPacketLength . fromIntegral $ 10 + BL.length bsk+ putWord8 pv -- must be 3+ putLazyByteString (unEOKI eokeyid) -- must be 8 octets+ putWord8 $ fromIntegral . fromFVal $ pka+ putLazyByteString bsk+putPkt+ ( PKESKPkt+ ( PKESKPayloadV6Packet+ (PKESKPayloadV6 recipientKeyIdentifier pka esk)+ )+ ) = do+ putWord8 (0xc0 .|. 1)+ let keyIdentifierLen = BL.length recipientKeyIdentifier+ when (keyIdentifierLen > 255) $+ error "PKESK v6 recipient key identifier must fit in one octet"+ putPacketLength . fromIntegral $+ 3 + keyIdentifierLen + BL.length esk+ putWord8 6+ putWord8 (fromIntegral keyIdentifierLen)+ putLazyByteString recipientKeyIdentifier+ putWord8 $ fromIntegral . fromFVal $ pka+ putLazyByteString esk+putPkt (SignaturePkt sp) = do+ putWord8 (0xc0 .|. 2)+ let bs = runPut $ put sp+ putLengthThenPayload bs+putPkt (SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 symalgo s2k mesk))) = do+ putWord8 (0xc0 .|. 3)+ let bs2k = fromS2K s2k+ let bsk = fromMaybe BL.empty mesk+ putPacketLength . fromIntegral $+ 2 + BL.length bs2k + BL.length bsk+ putWord8 4+ putWord8 $ fromIntegral . fromFVal $ symalgo+ putLazyByteString bs2k+ putLazyByteString bsk+putPkt+ ( SKESKPkt+ (SKESKPayloadV6Packet (SKESKPayloadV6 symalgo aead s2k iv esk tag))+ ) = do+ putWord8 (0xc0 .|. 3)+ let bs2k = fromS2K s2k+ let params =+ BL.pack+ [ fromIntegral (fromFVal symalgo)+ , fromIntegral (fromFVal aead)+ , fromIntegral (BL.length bs2k)+ ]+ <> bs2k+ <> iv+ putPacketLength . fromIntegral $+ 2 + BL.length params + BL.length esk + BL.length tag+ putWord8 6+ putWord8 (fromIntegral (BL.length params))+ putLazyByteString params+ putLazyByteString esk+ putLazyByteString tag+putPkt+ ( OnePassSignaturePkt+ (OPSPayloadV3Packet (OPSPayloadV3 pv sigtype ha pka skeyid nested))+ ) = do+ putWord8 (0xc0 .|. 4)+ let bs =+ runPut $ do+ putWord8 pv -- should be 3+ putWord8 $ fromIntegral . fromFVal $ sigtype+ putWord8 $ fromIntegral . fromFVal $ ha+ putWord8 $ fromIntegral . fromFVal $ pka+ putLazyByteString (unEOKI skeyid)+ putWord8 . fromIntegral . fromEnum $ not nested+ putLengthThenPayload bs+putPkt+ ( OnePassSignaturePkt+ ( OPSPayloadV6Packet+ (OPSPayloadV6 sigtype ha pka salt signerFingerprint nested)+ )+ ) = do+ putWord8 (0xc0 .|. 4)+ let saltBytes = unSignatureSalt salt+ saltSize = BL.length saltBytes+ expectedSaltSize =+ maybe+ ( error+ ( "signature hash algorithm does not define a V6 salt size: "+ ++ show ha+ )+ )+ id+ (v6SaltSizeForHashAlgorithm ha)+ when (fromIntegral saltSize /= expectedSaltSize) $+ error+ ( "OPS v6 salt size mismatch for "+ ++ show ha+ ++ ": expected "+ ++ show expectedSaltSize+ ++ ", got "+ ++ show saltSize+ )+ when (BL.length signerFingerprint /= 32) $+ error "OPS v6 signer fingerprint must be exactly 32 octets"+ let bs =+ runPut $ do+ putWord8 6+ putWord8 $ fromIntegral . fromFVal $ sigtype+ putWord8 $ fromIntegral . fromFVal $ ha+ putWord8 $ fromIntegral . fromFVal $ pka+ putWord8 (fromIntegral saltSize)+ putLazyByteString saltBytes+ putLazyByteString signerFingerprint+ putWord8 . fromIntegral . fromEnum $ not nested+ putLengthThenPayload bs+putPkt (SecretKeyPkt pkp ska) = do+ putWord8 (0xc0 .|. 5)+ let bs = runPut (putPKPayload pkp >> putSKAddendumForPKPayload pkp ska)+ putLengthThenPayload bs+putPkt (PublicKeyPkt pkp) = do+ putWord8 (0xc0 .|. 6)+ let bs = runPut $ putPKPayload pkp+ putLengthThenPayload bs+putPkt (SecretSubkeyPkt pkp ska) = do+ putWord8 (0xc0 .|. 7)+ let bs = runPut (putPKPayload pkp >> putSKAddendumForPKPayload pkp ska)+ putLengthThenPayload bs+putPkt (CompressedDataPkt ca cdata) = do+ putWord8 (0xc0 .|. 8)+ let bs =+ runPut $ do+ putWord8 $ fromIntegral . fromFVal $ ca+ putLazyByteString cdata+ putLengthThenPayload bs+putPkt (SymEncDataPkt b) = do+ putWord8 (0xc0 .|. 9)+ putLengthThenPayload b+putPkt (MarkerPkt b) = do+ putWord8 (0xc0 .|. 10)+ putLengthThenPayload b+putPkt (LiteralDataPkt dt fn ts b) = do+ putWord8 (0xc0 .|. 11)+ let bs =+ runPut $ do+ putWord8 $ fromIntegral . fromFVal $ dt+ putWord8 $ fromIntegral . BL.length $ fn+ putLazyByteString fn+ putWord32be . unThirtyTwoBitTimeStamp $ ts+ putLazyByteString b+ putLengthThenPayload bs+putPkt (TrustPkt b) = do+ putWord8 (0xc0 .|. 12)+ putLengthThenPayload b+putPkt (UserIdPkt u) = do+ putWord8 (0xc0 .|. 13)+ let bs = encodeUtf8 u+ putPacketLength . fromIntegral $ B.length bs+ putByteString bs+putPkt (PublicSubkeyPkt pkp) = do+ putWord8 (0xc0 .|. 14)+ let bs = runPut $ putPKPayload pkp+ putLengthThenPayload bs+putPkt (UserAttributePkt us) = do+ putWord8 (0xc0 .|. 17)+ let bs = runPut $ mapM_ put us+ putLengthThenPayload bs+putPkt (SymEncIntegrityProtectedDataPkt (SEIPD1 pv b)) = do+ putWord8 (0xc0 .|. 18)+ putPacketLength . fromIntegral $ BL.length b + 1+ putWord8 pv -- should be 1+ putLazyByteString b+putPkt+ ( SymEncIntegrityProtectedDataPkt+ (SEIPD2 symalgo aeadalgo chunkSize salt b)+ ) = do+ when (B.length (unSalt salt) /= 32) $+ error "SEIPD v2 salt must be exactly 32 octets"+ when (chunkSize > 16) $+ error "SEIPD v2 chunk size octet must be between 0 and 16"+ case symalgo of+ OtherSA _ -> error "SEIPD v2 requires a known symmetric algorithm"+ Plaintext -> error "SEIPD v2 cannot use plaintext cipher"+ _ -> return ()+ case aeadalgo of+ OtherAEADAlgo _ -> error "SEIPD v2 requires a known AEAD algorithm"+ _ -> return ()+ putWord8 (0xc0 .|. 18)+ putPacketLength . fromIntegral $ BL.length b + 36+ putWord8 2+ putWord8 (fromFVal symalgo)+ putWord8 (fromFVal aeadalgo)+ putWord8 chunkSize+ putByteString (unSalt salt)+ putLazyByteString b+putPkt (ModificationDetectionCodePkt hash) = do+ putWord8 (0xc0 .|. 19)+ putLengthThenPayload hash+putPkt (PaddingPkt padding) = do+ putWord8 (0xc0 .|. 21)+ putLengthThenPayload padding+putPkt (OtherPacketPkt t payload) = do+ when (t > 63) $+ error+ ("cannot serialize OtherPacket packet tag > 63: " ++ show t)+ putWord8 (0xc0 .|. t)+ putLengthThenPayload payload+putPkt (BrokenPacketPkt _ t payload) = putPkt (OtherPacketPkt t payload)++{- | Validate a packet before serialization to catch constraint violations early.+Returns Left with descriptive error if validation fails.+-}+validatePkt :: Pkt -> Either String ()+validatePkt+ ( PKESKPkt+ (PKESKPayloadV6Packet (PKESKPayloadV6 recipientKeyIdentifier _ _))+ ) = do+ let keyIdentifierLen = BL.length recipientKeyIdentifier+ when (keyIdentifierLen > 255) $+ Left+ "PKESK v6 recipient key identifier must fit in one octet (max 255 bytes)"+ Right ()+validatePkt+ ( OnePassSignaturePkt+ (OPSPayloadV6Packet (OPSPayloadV6 _ ha _ salt signerFingerprint _))+ ) = do+ let saltBytes = unSignatureSalt salt+ saltSize = BL.length saltBytes+ expectedSaltSize <-+ case v6SaltSizeForHashAlgorithm ha of+ Nothing ->+ Left $+ "signature hash algorithm does not define a V6 salt size: "+ ++ show ha+ Just sz -> Right sz+ when (fromIntegral saltSize /= expectedSaltSize) $+ Left+ ( "OPS v6 salt size mismatch for "+ ++ show ha+ ++ ": expected "+ ++ show expectedSaltSize+ ++ ", got "+ ++ show saltSize+ )+ when (BL.length signerFingerprint /= 32) $+ Left "OPS v6 signer fingerprint must be exactly 32 octets"+ Right ()+validatePkt+ ( SymEncIntegrityProtectedDataPkt+ (SEIPD2 symalgo aeadalgo chunkSize salt _)+ ) = do+ when (B.length (unSalt salt) /= 32) $+ Left "SEIPD v2 salt must be exactly 32 octets"+ when (chunkSize > 16) $+ Left "SEIPD v2 chunk size octet must be between 0 and 16"+ case symalgo of+ OtherSA _ -> Left "SEIPD v2 requires a known symmetric algorithm"+ Plaintext -> Left "SEIPD v2 cannot use plaintext cipher"+ _ -> Right ()+ case aeadalgo of+ OtherAEADAlgo _ -> Left "SEIPD v2 requires a known AEAD algorithm"+ _ -> Right ()+validatePkt (OtherPacketPkt t _) = do+ when (t > 63) $+ Left ("cannot serialize OtherPacket packet tag > 63: " ++ show t)+ Right ()+validatePkt _ = Right ()++{- | Serialize a packet with explicit validation and error handling.+Validates constraints before calling putPkt to ensure errors are caught early.+-}+putPktEither :: Pkt -> Either String Put+putPktEither pkt = case validatePkt pkt of+ Left err -> Left err+ Right () -> Right (putPkt pkt)++putLengthThenPayload :: ByteString -> Put+putLengthThenPayload bs = do+ let len = BL.length bs+ if len < fromIntegral (0x100000000 :: Integer)+ then do+ putPacketLength (fromIntegral len)+ putLazyByteString bs+ else putPartialLengthPayload bs+ where+ maxPartialChunkSize :: Int64+ maxPartialChunkSize = 1 `shiftL` (30 :: Int)+ putPartialLengthPayload :: ByteString -> Put+ putPartialLengthPayload payload+ | BL.length payload > maxPartialChunkSize = do+ let (chunk, rest) = BL.splitAt maxPartialChunkSize payload+ putPartialLength 30+ putLazyByteString chunk+ putPartialLengthPayload rest+ | otherwise = do+ putPacketLength (fromIntegral (BL.length payload))+ putLazyByteString payload++validateSEIPDv2Header+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> Word8+ -> ByteString+ -> Get ()+validateSEIPDv2Header symalgo aeadalgo chunkSize encrypted = do+ when (chunkSize > 16) $+ fail "SEIPD v2 chunk size octet must be between 0 and 16"+ when (BL.null encrypted) $+ fail+ "SEIPD v2 payload is missing encrypted data and final authentication tag"+ case symalgo of+ OtherSA _ -> fail "SEIPD v2 requires a known symmetric algorithm"+ Plaintext -> fail "SEIPD v2 cannot use plaintext cipher"+ _ -> return ()+ case aeadalgo of+ OtherAEADAlgo _ -> fail "SEIPD v2 requires a known AEAD algorithm"+ _ -> return ()++getMPI :: Get MPI+getMPI = do+ mpilen <- getWord16be+ bs <- getByteString (fromIntegral (mpilen + 7) `div` 8)+ return $ MPI (os2ip bs)++getPubkey :: PubKeyAlgorithm -> Get PKey+getPubkey RSA = do+ MPI n <- get+ MPI e <- get+ return $+ RSAPubKey+ ( RSA_PublicKey+ (R.PublicKey (fromIntegral . B.length . i2osp $ n) n e)+ )+getPubkey DeprecatedRSAEncryptOnly = getPubkey RSA+getPubkey DeprecatedRSASignOnly = getPubkey RSA+getPubkey DSA = do+ MPI p <- get+ MPI q <- get+ MPI g <- get+ MPI y <- get+ return $+ DSAPubKey (DSA_PublicKey (D.PublicKey (D.Params p g q) y))+getPubkey ElgamalEncryptOnly = getPubkey ForbiddenElgamal+getPubkey ForbiddenElgamal = do+ MPI p <- get+ MPI g <- get+ MPI y <- get+ return $ ElGamalPubKey p g y+getPubkey ECDSA = do+ curvelength <- getWord8+ when (curvelength == 0 || curvelength == 0xff) $+ fail "invalid ECC curve OID length octet (reserved value)"+ curveoid <- getByteString (fromIntegral curvelength)+ MPI mpi <- getMPI+ case curveoidBSToCurve curveoid of+ Left e -> fail e+ Right Curve25519 ->+ EdDSAPubKey P.EdSigningCurve25519+ <$> ( PrefixedNativeEPoint+ <$> validatePrefixedNativePoint 32 "Curve25519Legacy" mpi+ )+ Right curve ->+ case bs2Point (i2osp mpi) of+ Left e -> fail e+ Right point ->+ return+ . ECDSAPubKey+ . ECDSA_PublicKey+ . ECDSA.PublicKey (curve2Curve curve)+ $ point+getPubkey ECDH = do+ ed <- getPubkey ECDSA -- could be an ECDSA or an EdDSA+ kdflen <- getWord8+ when (kdflen == 0 || kdflen == 0xff) $+ fail "invalid ECDH KDF field length octet (reserved value)"+ when (kdflen /= 3) $+ fail ("invalid ECDH KDF field length: " ++ show kdflen)+ one <- getWord8+ when (one /= 1) $+ fail ("invalid ECDH KDF reserved octet: " ++ show one)+ kdfHA <- get+ kdfSA <- get+ return $ ECDHPubKey ed kdfHA kdfSA+getPubkey EdDSA = do+ curvelength <- getWord8+ when (curvelength == 0 || curvelength == 0xff) $+ fail "invalid EdDSA curve OID length octet (reserved value)"+ curveoid <- getByteString (fromIntegral curvelength)+ MPI mpi <- getMPI+ case curveoidBSToEdSigningCurve curveoid of+ Left e -> fail e+ Right P.EdSigningCurve25519 ->+ EdDSAPubKey P.EdSigningCurve25519+ <$> ( PrefixedNativeEPoint+ <$> validatePrefixedNativePoint 32 "Ed25519Legacy" mpi+ )+ Right P.EdSigningCurve448 ->+ EdDSAPubKey P.EdSigningCurve448+ <$> ( PrefixedNativeEPoint+ <$> validatePrefixedNativePoint 57 "Ed448Legacy" mpi+ )+getPubkey pka+ | pka == BTypes.Ed25519 =+ parseFixedLengthOrLegacyPubkey+ 32+ ( EdDSAPubKey P.EdSigningCurve25519+ . NativeEPoint+ . EPoint+ . os2ip+ . BL.toStrict+ )+ (getPubkey EdDSA)+getPubkey pka+ | pka == BTypes.Ed448 =+ parseFixedLengthOrLegacyPubkey+ 57+ ( EdDSAPubKey P.EdSigningCurve448+ . NativeEPoint+ . EPoint+ . os2ip+ . BL.toStrict+ )+ (getPubkey EdDSA)+getPubkey X25519 =+ parseFixedLengthOrLegacyPubkey+ 32+ ( EdDSAPubKey P.EdSigningCurve25519+ . NativeEPoint+ . EPoint+ . os2ip+ . BL.toStrict+ )+ (getPubkey ECDH)+getPubkey X448 =+ parseFixedLengthOrLegacyPubkey+ 56+ ( EdDSAPubKey P.EdSigningCurve448+ . NativeEPoint+ . EPoint+ . os2ip+ . BL.toStrict+ )+ (getPubkey ECDH)+getPubkey MLKEM768X25519 = MLKEMPubKey . BL.toStrict <$> getRemainingLazyByteString+getPubkey MLKEM1024X448 = MLKEMPubKey . BL.toStrict <$> getRemainingLazyByteString+getPubkey MLDSA65Ed25519 = MLDSAPubKey . BL.toStrict <$> getRemainingLazyByteString+getPubkey MLDSA87Ed448 = MLDSAPubKey . BL.toStrict <$> getRemainingLazyByteString+getPubkey SLHDSASHAKE128s = SLHDSAPubKey . BL.toStrict <$> getRemainingLazyByteString+getPubkey SLHDSASHAKE128f = SLHDSAPubKey . BL.toStrict <$> getRemainingLazyByteString+getPubkey SLHDSASHAKE256s = SLHDSAPubKey . BL.toStrict <$> getRemainingLazyByteString+getPubkey _ = UnknownPKey <$> getRemainingLazyByteString++parseFixedLengthOrLegacyPubkey+ :: Int64 -> (BL.ByteString -> PKey) -> Get PKey -> Get PKey+parseFixedLengthOrLegacyPubkey expectedLen decodeFixed legacyParser = do+ remaining <- lookAhead getRemainingLazyByteString+ if BL.length remaining == expectedLen+ then decodeFixed <$> getLazyByteString expectedLen+ else legacyParser++getPubkeyV6 :: PubKeyAlgorithm -> Get PKey+getPubkeyV6 pka+ | pka == BTypes.Ed25519 = do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ when (B.length bs /= 32) $+ fail "invalid v6 Ed25519 public key length"+ return $+ EdDSAPubKey+ P.EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip bs)))+ | pka == BTypes.Ed448 = do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ when (B.length bs /= 57) $+ fail "invalid v6 Ed448 public key length"+ return $+ EdDSAPubKey+ P.EdSigningCurve448+ (NativeEPoint (EPoint (os2ip bs)))+ | pka == BTypes.X25519 = do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ when (B.length bs /= 32) $+ fail "invalid v6 X25519 public key length"+ return $+ EdDSAPubKey+ P.EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip bs)))+ | pka == BTypes.X448 = do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ when (B.length bs /= 56) $+ fail "invalid v6 X448 public key length"+ return $+ EdDSAPubKey+ P.EdSigningCurve448+ (NativeEPoint (EPoint (os2ip bs)))+ | pka == MLKEM768X25519 = do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ return $ MLKEMPubKey bs+ | pka == MLKEM1024X448 = do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ return $ MLKEMPubKey bs+ | pka == MLDSA65Ed25519 = do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ return $ MLDSAPubKey bs+ | pka == MLDSA87Ed448 = do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ return $ MLDSAPubKey bs+ | pka == SLHDSASHAKE128s = do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ return $ SLHDSAPubKey bs+ | pka == SLHDSASHAKE128f = do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ return $ SLHDSAPubKey bs+ | pka == SLHDSASHAKE256s = do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ return $ SLHDSAPubKey bs+ | otherwise = getPubkey pka++bs2Point :: B.ByteString -> Either String ECDSA.PublicPoint+bs2Point bs =+ if B.null bs+ then Left "empty EC point encoding"+ else+ let xy = B.drop 1 bs+ l = B.length xy+ in if B.head bs /= 0x04+ then Left $ "unknown type of point: " ++ show (B.unpack bs)+ else+ if odd l+ then+ Left "malformed EC point encoding: odd coordinate payload length"+ else+ return+ ( uncurry+ ECCT.Point+ ((os2ip *** os2ip) (B.splitAt (div l 2) xy))+ )++putPubkey :: PKey -> Put+putPubkey (UnknownPKey bs) = putLazyByteString bs+putPubkey (MLKEMPubKey bs) = putLazyByteString (BL.fromStrict bs)+putPubkey (MLDSAPubKey bs) = putLazyByteString (BL.fromStrict bs)+putPubkey (SLHDSAPubKey bs) = putLazyByteString (BL.fromStrict bs)+putPubkey p@(ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _))) =+ let Right curveoidbs = curveToCurveoidBS (curveFromCurve curve)+ in putCurveOID curveoidbs+ >> mapM_ put (pubkeyToMPIs p)+putPubkey+ p@( ECDHPubKey+ (ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _)))+ kha+ ksa+ ) =+ let Right curveoidbs = curveToCurveoidBS (curveFromCurve curve)+ in putCurveOID curveoidbs+ >> mapM_ put (pubkeyToMPIs p)+ >> putECDHKDFParams kha ksa+putPubkey p@(ECDHPubKey (EdDSAPubKey curve (PrefixedNativeEPoint _)) kha ksa) =+ let Right curveoidbs = curveToCurveoidBS (ed2ec curve)+ in putCurveOID curveoidbs+ >> mapM_ put (pubkeyToMPIs p)+ >> putECDHKDFParams kha ksa+ where+ ed2ec P.EdSigningCurve25519 = Curve25519+ ed2ec P.EdSigningCurve448 = Curve448+putPubkey p@(EdDSAPubKey curve (PrefixedNativeEPoint _)) =+ let Right curveoidbs = edSigningCurveToCurveoidBS curve+ in putCurveOID curveoidbs+ >> mapM_ put (pubkeyToMPIs p)+putPubkey (ECDHPubKey (EdDSAPubKey curve (NativeEPoint _)) _ _) =+ error+ ( "legacy ECDH serialization requires a prefixed-native "+ ++ show curve+ ++ " point"+ )+putPubkey (EdDSAPubKey curve (NativeEPoint _)) =+ error+ ( "legacy EdDSA serialization requires a prefixed-native "+ ++ show curve+ ++ " point"+ )+putPubkey p = mapM_ put (pubkeyToMPIs p)++putPubkeyV6 :: PKey -> Put+putPubkeyV6 (EdDSAPubKey P.EdSigningCurve25519 (NativeEPoint (EPoint x))) = do+ let bs = fixedLengthOctets 32 x+ putWord32be . fromIntegral . B.length $ bs+ putByteString bs+putPubkeyV6 (EdDSAPubKey P.EdSigningCurve448 (NativeEPoint (EPoint x))) = do+ let bs = fixedLengthOctets 57 x+ putWord32be . fromIntegral . B.length $ bs+ putByteString bs+putPubkeyV6+ ( ECDHPubKey+ (EdDSAPubKey P.EdSigningCurve25519 (NativeEPoint (EPoint x)))+ kha+ ksa+ ) = do+ let bs = fixedLengthOctets 32 x+ putWord32be . fromIntegral . B.length $ bs+ putByteString bs+ put kha+ put ksa+putPubkeyV6+ ( ECDHPubKey+ (EdDSAPubKey P.EdSigningCurve448 (NativeEPoint (EPoint x)))+ kha+ ksa+ ) = do+ let bs = fixedLengthOctets 56 x+ putWord32be . fromIntegral . B.length $ bs+ putByteString bs+ put kha+ put ksa+putPubkeyV6 (MLKEMPubKey bs) = do+ putWord32be . fromIntegral . B.length $ bs+ putByteString bs+putPubkeyV6 (MLDSAPubKey bs) = do+ putWord32be . fromIntegral . B.length $ bs+ putByteString bs+putPubkeyV6 (SLHDSAPubKey bs) = do+ putWord32be . fromIntegral . B.length $ bs+ putByteString bs+putPubkeyV6 p = putPubkey p++fixedLengthOctets :: Int -> Integer -> B.ByteString+fixedLengthOctets targetLen x =+ let bs = i2osp x+ in if B.length bs > targetLen+ then+ error+ ( "public key element does not fit in "+ ++ show targetLen+ ++ " octets"+ )+ else B.replicate (targetLen - B.length bs) 0 <> bs++validatePrefixedNativePoint+ :: Int -> String -> Integer -> Get EPoint+validatePrefixedNativePoint targetLen label i =+ let bs = i2osp i+ in if B.length bs /= targetLen + 1+ then+ fail+ ( "invalid "+ ++ label+ ++ " public key length: expected "+ ++ show (targetLen + 1)+ ++ " octets with 0x40 prefix, got "+ ++ show (B.length bs)+ )+ else+ if B.head bs /= 0x40+ then+ fail ("invalid " ++ label ++ " public key: missing 0x40 prefix")+ else pure (EPoint i)++putCurveOID :: B.ByteString -> Put+putCurveOID oid = do+ let oidLength = B.length oid+ when (oidLength == 0 || oidLength == 0xff) $+ error "curve OID length cannot use reserved values 0 or 255"+ putWord8 (fromIntegral oidLength)+ putByteString oid++putECDHKDFParams :: HashAlgorithm -> SymmetricAlgorithm -> Put+putECDHKDFParams kdfHA kdfSA = do+ let kdfLengthOctet = 0x03+ when (kdfLengthOctet == 0 || kdfLengthOctet == 0xff) $+ error "ECDH KDF field length cannot use reserved values 0 or 255"+ putWord8 kdfLengthOctet+ putWord8 0x01+ put kdfHA+ put kdfSA++parseOPSNestedFlag :: Word8 -> Get NestedFlag+parseOPSNestedFlag 0 = pure True+parseOPSNestedFlag 1 = pure False+parseOPSNestedFlag other =+ fail ("invalid OPS nested flag octet: " ++ show other)++getSecretKey :: SomePKPayload -> Get SKey+getSecretKey pkp+ | _pkalgo pkp+ `elem` [RSA, DeprecatedRSAEncryptOnly, DeprecatedRSASignOnly] = do+ MPI d <- get+ MPI p <- get+ MPI q <- get+ MPI _ <- get -- u+ case inverse q p of+ Nothing -> fail "invalid RSA secret key: q has no inverse modulo p"+ Just qinv -> do+ let dP = d `mod` (p - 1)+ dQ = d `mod` (q - 1)+ pub = (\(RSAPubKey (RSA_PublicKey x)) -> x) (_pubkey pkp)+ return $+ RSAPrivateKey+ (RSA_PrivateKey (R.PrivateKey pub d p q dP dQ qinv))+ | _pkalgo pkp == DSA = do+ MPI x <- get+ return $+ DSAPrivateKey (DSA_PrivateKey (D.PrivateKey (D.Params 0 0 0) x))+ | _pkalgo pkp `elem` [ElgamalEncryptOnly, ForbiddenElgamal] = do+ MPI x <- get+ return $ ElGamalPrivateKey x+ | _pkalgo pkp == ECDSA = do+ let pubcurve =+ (\(ECDSAPubKey (ECDSA_PublicKey p)) -> ECDSA.public_curve p)+ (_pubkey pkp)+ getECDSAScalarPrivateKey pubcurve+ | _pkalgo pkp == ECDH =+ do+ pubcurve <- ecdhPrivateCurveFromPKPayload pkp+ getECDHScalarPrivateKey pubcurve+ | _pkalgo pkp == X25519 = do+ if _keyVersion pkp == V6+ then do+ sk <- getByteString 32+ return $ X25519PrivateKey sk+ else do+ pubcurve <- ecdhPrivateCurveFromPKPayload pkp+ getECDHScalarPrivateKey pubcurve+ | _pkalgo pkp == X448 = do+ if _keyVersion pkp == V6+ then do+ sk <- getByteString 56+ return $ X448PrivateKey sk+ else UnknownSKey <$> getRemainingLazyByteString+ | _pkalgo pkp == EdDSA = do+ if _keyVersion pkp == V6+ then do+ case _pubkey pkp of+ EdDSAPubKey P.EdSigningCurve25519 _ -> EdDSAPrivateKey P.EdSigningCurve25519 <$> getByteString 32+ EdDSAPubKey P.EdSigningCurve448 _ -> EdDSAPrivateKey P.EdSigningCurve448 <$> getByteString 57+ _ -> UnknownSKey <$> getRemainingLazyByteString+ else do+ MPI x <- get+ case _pubkey pkp of+ EdDSAPubKey P.EdSigningCurve25519 _ ->+ return $+ EdDSAPrivateKey P.EdSigningCurve25519 (leftPadTo 32 (i2osp x))+ EdDSAPubKey P.EdSigningCurve448 _ ->+ return $+ EdDSAPrivateKey P.EdSigningCurve448 (leftPadTo 57 (i2osp x))+ _ -> return $ UnknownSKey (BL.fromStrict (i2osp x))+ | _pkalgo pkp `elem` [MLKEM768X25519, MLKEM1024X448] = do+ if _keyVersion pkp == V6+ then do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ return $ MLKEMPrivateKey bs+ else UnknownSKey <$> getRemainingLazyByteString+ | _pkalgo pkp `elem` [MLDSA65Ed25519, MLDSA87Ed448] = do+ if _keyVersion pkp == V6+ then do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ return $ MLDSAPrivateKey bs+ else UnknownSKey <$> getRemainingLazyByteString+ | _pkalgo pkp+ `elem` [SLHDSASHAKE128s, SLHDSASHAKE128f, SLHDSASHAKE256s] = do+ if _keyVersion pkp == V6+ then do+ len <- getWord32be+ bs <- getByteString (fromIntegral len)+ return $ SLHDSAPrivateKey bs+ else UnknownSKey <$> getRemainingLazyByteString+ | otherwise = UnknownSKey <$> getRemainingLazyByteString++getECDSAScalarPrivateKey :: ECCT.Curve -> Get SKey+getECDSAScalarPrivateKey curve = do+ MPI pn <- get+ pure $+ ECDSAPrivateKey (ECDSA_PrivateKey (ECDSA.PrivateKey curve pn))++getECDHScalarPrivateKey :: ECCT.Curve -> Get SKey+getECDHScalarPrivateKey curve = do+ MPI pn <- get+ pure $+ ECDHPrivateKey (ECDSA_PrivateKey (ECDSA.PrivateKey curve pn))++ecdhPrivateCurveFromPKPayload :: SomePKPayload -> Get ECCT.Curve+ecdhPrivateCurveFromPKPayload pkp =+ case _pubkey pkp of+ ECDHPubKey (ECDSAPubKey (ECDSA_PublicKey p)) _ _ ->+ pure (ECDSA.public_curve p)+ ECDHPubKey (EdDSAPubKey P.EdSigningCurve25519 _) _ _ ->+ pure (curve2Curve Curve25519)+ ECDHPubKey (EdDSAPubKey P.EdSigningCurve448 _) _ _ ->+ pure (curve2Curve Curve448)+ other ->+ fail+ ( "ECDH/X25519 secret key requires an ECDH public key packet, got "+ ++ show other+ )++putSKey :: SKey -> Either String Put+putSKey (RSAPrivateKey (RSA_PrivateKey (R.PrivateKey _ d p q _ _ _))) =+ case inverse q p of+ Just u ->+ Right (put (MPI d) >> put (MPI p) >> put (MPI q) >> put (MPI u))+ Nothing ->+ Left+ "putSKey: invalid RSA key — q has no multiplicative inverse mod p (key is mathematically broken)"+putSKey (DSAPrivateKey (DSA_PrivateKey (D.PrivateKey _ x))) =+ Right (put (MPI x))+putSKey (ElGamalPrivateKey x) =+ Right (put (MPI x))+putSKey (ECDHPrivateKey (ECDSA_PrivateKey (ECDSA.PrivateKey _ d))) =+ Right (put (MPI d))+putSKey (ECDSAPrivateKey (ECDSA_PrivateKey (ECDSA.PrivateKey _ d))) =+ Right (put (MPI d))+putSKey (EdDSAPrivateKey P.EdSigningCurve25519 sk) = Right (putByteString sk)+putSKey (EdDSAPrivateKey P.EdSigningCurve448 sk) = Right (putByteString sk)+putSKey (X25519PrivateKey sk) = Right (putByteString sk)+putSKey (X448PrivateKey sk) = Right (putByteString sk)+putSKey (MLKEMPrivateKey sk) = Right (putLazyByteString (BL.fromStrict sk))+putSKey (MLDSAPrivateKey sk) = Right (putLazyByteString (BL.fromStrict sk))+putSKey (SLHDSAPrivateKey sk) = Right (putLazyByteString (BL.fromStrict sk))+putSKey (UnknownSKey bs) = Right (putLazyByteString bs)++putSKeyForPKPayload :: SomePKPayload -> SKey -> Either String Put+putSKeyForPKPayload pkp sk@(EdDSAPrivateKey _ bs)+ | _keyVersion pkp == V6 = putSKey sk+ | otherwise = Right (put (MPI (os2ip bs)))+putSKeyForPKPayload _ sk = putSKey sk++putMPI :: MPI -> Put+putMPI (MPI i) = do+ let bs = i2osp i+ putWord16be . fromIntegral . numBits $ i+ putByteString bs++data PKPayloadReadCase where+ PKPayloadReadCaseV3+ :: V3Expiration -> PubKeyAlgorithm -> PKPayloadReadCase+ PKPayloadReadCaseV4 :: PubKeyAlgorithm -> PKPayloadReadCase+ PKPayloadReadCaseV6 :: PubKeyAlgorithm -> PKPayloadReadCase++pkPayloadReadCase :: Word8 -> Get PKPayloadReadCase+pkPayloadReadCase version =+ case version of+ 2 -> do+ v3e <- getWord16be+ pka <- get+ pure (PKPayloadReadCaseV3 v3e pka)+ 3 -> do+ v3e <- getWord16be+ pka <- get+ pure (PKPayloadReadCaseV3 v3e pka)+ 4 -> PKPayloadReadCaseV4 <$> get+ 6 -> PKPayloadReadCaseV6 <$> get+ _ -> fail ("unsupported key packet version " ++ show version)++getPKPayload :: Get SomePKPayload+getPKPayload = do+ version <- getWord8+ ctime <- fmap ThirtyTwoBitTimeStamp getWord32be+ readCase <- pkPayloadReadCase version+ case readCase of+ PKPayloadReadCaseV3 v3e pka -> do+ pk <- getPubkey pka+ pure $! PKPayload DeprecatedV3 ctime v3e pka pk+ PKPayloadReadCaseV4 pka -> do+ pk <- getPubkey pka+ pure $! PKPayload V4 ctime 0 pka pk+ PKPayloadReadCaseV6 pka -> do+ pk <- getPubkeyV6 pka+ pure $! PKPayload V6 ctime 0 pka pk++data PKPayloadWriteCase where+ PKPayloadWriteCaseV3+ :: PKPayload 'DeprecatedV3 -> PKPayloadWriteCase+ PKPayloadWriteCaseV4 :: PKPayload 'V4 -> PKPayloadWriteCase+ PKPayloadWriteCaseV6 :: PKPayload 'V6 -> PKPayloadWriteCase++pkPayloadWriteCase :: SomePKPayload -> PKPayloadWriteCase+pkPayloadWriteCase (SomePKPayload pkp) =+ case pkp of+ PKPayloadV3 {} -> PKPayloadWriteCaseV3 pkp+ PKPayloadV4 {} -> PKPayloadWriteCaseV4 pkp+ PKPayloadV6 {} -> PKPayloadWriteCaseV6 pkp++putPKPayload :: SomePKPayload -> Put+putPKPayload pkpSome =+ case pkPayloadWriteCase pkpSome of+ PKPayloadWriteCaseV3 (PKPayloadV3 ctime v3e pka pk) -> do+ putWord8 3+ putWord32be . unThirtyTwoBitTimeStamp $ ctime+ putWord16be v3e+ put pka+ putPubkey pk+ PKPayloadWriteCaseV4 (PKPayloadV4 ctime pka pk) -> do+ putWord8 4+ putWord32be . unThirtyTwoBitTimeStamp $ ctime+ put pka+ putPubkeyV4ForAlgorithm pka pk+ PKPayloadWriteCaseV6 (PKPayloadV6 ctime pka pk) -> do+ putWord8 6+ putWord32be . unThirtyTwoBitTimeStamp $ ctime+ put pka+ putPubkeyV6 pk++putPubkeyV4ForAlgorithm :: PubKeyAlgorithm -> PKey -> Put+putPubkeyV4ForAlgorithm pka pk+ | pka == BTypes.Ed25519 =+ putPubkeyV4Fixed 32 P.EdSigningCurve25519 pk+ | pka == BTypes.Ed448 =+ putPubkeyV4Fixed 57 P.EdSigningCurve448 pk+ | pka == BTypes.X25519 =+ putPubkeyV4Fixed 32 P.EdSigningCurve25519 pk+ | pka == BTypes.X448 = putPubkeyV4Fixed 56 P.EdSigningCurve448 pk+ | otherwise = putPubkey pk++putPubkeyV4Fixed :: Int -> P.EdSigningCurve -> PKey -> Put+putPubkeyV4Fixed targetLen expectedCurve (EdDSAPubKey curve (NativeEPoint (EPoint x)))+ | curve == expectedCurve =+ putByteString (fixedLengthOctets targetLen x)+putPubkeyV4Fixed _ _ pk = putPubkey pk++getSKAddendum :: SomePKPayload -> Get SKAddendum+getSKAddendum (SomePKPayload pkp) =+ toSKAddendum <$> getSKAddendumTyped pkp++getSKAddendumTyped :: PKPayload v -> Get (SKAddendumV v)+getSKAddendumTyped pkp = do+ s2kusage <- getWord8+ let pkpSome = SomePKPayload pkp+ getLegacyS2KProtected constructor = do+ symencWord <- getWord8+ s2k <- getS2K+ let symenc = toFVal symencWord+ case s2k of+ OtherS2K _ _ -> return $ constructor symenc s2k mempty BL.empty+ _ -> do+ blockSize <- either fail pure (symEncBlockSize symenc)+ iv <- IV <$> getByteString blockSize+ encryptedblock <- getRemainingLazyByteString+ return $ constructor symenc s2k iv encryptedblock+ case s2kusage of+ 0 ->+ case pkp of+ PKPayloadV6 {} -> do+ sk <- getSecretKey pkpSome+ return (SKAUnencryptedV6 sk)+ PKPayloadV3 {} -> do+ rest <- lookAhead getRemainingLazyByteString+ secretLen <-+ case runGetOrFail+ ( do+ start <- bytesRead+ _ <- getSecretKey pkpSome+ end <- bytesRead+ pure (end - start)+ )+ rest of+ Left (_, _, err) -> fail err+ Right (_, _, len) -> pure len+ sk <- getSecretKey pkpSome+ checksum <- getWord16be+ let expectedChecksum =+ checksum16Bytes (BL.toStrict (BL.take secretLen rest))+ when (checksum /= expectedChecksum) $+ fail+ ( "legacy unencrypted secret-key checksum mismatch: expected "+ ++ show expectedChecksum+ ++ ", got "+ ++ show checksum+ )+ return (SKAUnencryptedLegacy sk checksum)+ PKPayloadV4 {} -> do+ rest <- lookAhead getRemainingLazyByteString+ secretLen <-+ case runGetOrFail+ ( do+ start <- bytesRead+ _ <- getSecretKey pkpSome+ end <- bytesRead+ pure (end - start)+ )+ rest of+ Left (_, _, err) -> fail err+ Right (_, _, len) -> pure len+ sk <- getSecretKey pkpSome+ checksum <- getWord16be+ let expectedChecksum =+ checksum16Bytes (BL.toStrict (BL.take secretLen rest))+ when (checksum /= expectedChecksum) $+ fail+ ( "legacy unencrypted secret-key checksum mismatch: expected "+ ++ show expectedChecksum+ ++ ", got "+ ++ show checksum+ )+ return (SKAUnencryptedLegacy sk checksum)+ 255 ->+ case pkp of+ PKPayloadV6 {} ->+ fail "v6 secret key packets MUST NOT use s2k usage 255"+ PKPayloadV3 {} ->+ getLegacyS2KProtected SKA16bit+ PKPayloadV4 {} ->+ getLegacyS2KProtected SKA16bit+ 254 ->+ case pkp of+ PKPayloadV6 {} -> do+ paramsLen <- getWord8+ params <- getLazyByteString (fromIntegral paramsLen)+ (symenc, s2k, iv) <-+ case runGetOrFail getV6CFBParams params of+ Left (_, _, err) -> fail err+ Right (rest, _, parsed)+ | not (BL.null rest) ->+ fail "unexpected trailing v6 CFB parameters"+ | otherwise -> pure parsed+ encryptedblock <- getRemainingLazyByteString+ return (SKASHA1V6 symenc s2k (IV iv) encryptedblock)+ PKPayloadV3 {} ->+ getLegacyS2KProtected SKASHA1Legacy+ PKPayloadV4 {} ->+ getLegacyS2KProtected SKASHA1Legacy+ where+ getV6CFBParams = do+ symencWord <- getWord8+ s2kLen <- getWord8+ s2kBytes <- getLazyByteString (fromIntegral s2kLen)+ s2k <-+ case runGetOrFail getS2K s2kBytes of+ Left (_, _, err) -> fail err+ Right (rest, _, parsed)+ | not (BL.null rest) ->+ fail "unexpected trailing bytes in v6 S2K specifier"+ | otherwise -> pure parsed+ iv <- getRemainingLazyByteString+ let symenc = toFVal symencWord+ blockSize <- either fail pure (symEncBlockSize symenc)+ when (BL.length iv /= fromIntegral blockSize) $+ fail "invalid v6 CFB IV length"+ pure (symenc, s2k, BL.toStrict iv)+ 253 ->+ case pkp of+ PKPayloadV6 {} -> do+ paramsLen <- getWord8+ params <- getLazyByteString (fromIntegral paramsLen)+ (symenc, aead, s2k, iv) <-+ case runGetOrFail getV6AEADParams params of+ Left (_, _, err) -> fail err+ Right (rest, _, parsed)+ | not (BL.null rest) ->+ fail "unexpected trailing v6 AEAD parameters"+ | otherwise -> pure parsed+ encryptedblock <- getRemainingLazyByteString+ return (SKAAEADV6 symenc aead s2k (IV iv) encryptedblock)+ PKPayloadV3 {} -> do+ (symenc, aead, s2k, iv) <- getLegacyAEADParams+ encryptedblock <- getRemainingLazyByteString+ return (SKAAEADLegacy symenc aead s2k (IV iv) encryptedblock)+ PKPayloadV4 {} -> do+ (symenc, aead, s2k, iv) <- getLegacyAEADParams+ encryptedblock <- getRemainingLazyByteString+ return (SKAAEADLegacy symenc aead s2k (IV iv) encryptedblock)+ where+ getV6AEADParams+ :: Get (SymmetricAlgorithm, AEADAlgorithm, S2K, B.ByteString)+ getV6AEADParams = do+ symencWord <- getWord8+ aeadWord <- getWord8+ s2kLen <- getWord8+ s2kBytes <- getLazyByteString (fromIntegral s2kLen)+ s2k <-+ case runGetOrFail getS2K s2kBytes of+ Left (_, _, err) -> fail err+ Right (rest, _, parsed)+ | not (BL.null rest) ->+ fail "unexpected trailing bytes in v6 S2K specifier"+ | otherwise -> pure parsed+ iv <- getRemainingLazyByteString+ let symenc = toFVal symencWord+ aead = toFVal aeadWord+ when (BL.length iv /= fromIntegral (aeadNonceSize aead)) $+ fail "invalid v6 AEAD IV length"+ pure (symenc, aead, s2k, BL.toStrict iv)+ -- v3/v4: no cumulative-params-length octet, no S2K-size octet+ getLegacyAEADParams+ :: Get (SymmetricAlgorithm, AEADAlgorithm, S2K, B.ByteString)+ getLegacyAEADParams = do+ symencWord <- getWord8+ aeadWord <- getWord8+ s2k <- getS2K+ let aead = toFVal aeadWord+ iv <-+ BL.toStrict+ <$> getLazyByteString (fromIntegral (aeadNonceSize aead))+ pure (toFVal symencWord, aead, s2k, iv)+ symenc ->+ case pkp of+ PKPayloadV6 {} -> do+ paramsLen <- getWord8+ iv <- getByteString (fromIntegral paramsLen)+ let symencAlg = toFVal symenc+ blockSize <- either fail pure (symEncBlockSize symencAlg)+ when (B.length iv /= blockSize) $+ fail "invalid v6 CFB IV length"+ encryptedblock <- getRemainingLazyByteString+ return (SKASymV6 symencAlg (IV iv) encryptedblock)+ PKPayloadV3 {} -> do+ blockSize <- either fail pure (symEncBlockSize (toFVal symenc))+ iv <- getByteString blockSize+ encryptedblock <- getRemainingLazyByteString+ return (SKASymLegacy (toFVal symenc) (IV iv) encryptedblock)+ PKPayloadV4 {} -> do+ blockSize <- either fail pure (symEncBlockSize (toFVal symenc))+ iv <- getByteString blockSize+ encryptedblock <- getRemainingLazyByteString+ return (SKASymLegacy (toFVal symenc) (IV iv) encryptedblock)++putSKAddendum :: SKAddendum -> Either String Put+putSKAddendum (SUS16bit symenc s2k iv encryptedblock) =+ Right $ do+ putWord8 255+ put symenc+ put s2k+ putByteString (unIV iv)+ putLazyByteString encryptedblock+putSKAddendum (SUSSHA1 symenc s2k iv encryptedblock) =+ Right $ do+ putWord8 254+ put symenc+ put s2k+ putByteString (unIV iv)+ putLazyByteString encryptedblock+putSKAddendum (SUSAEAD symenc aead s2k iv encryptedblock) =+ Right $ do+ putWord8 253+ put symenc+ putWord8 (fromFVal aead)+ put s2k+ putByteString (unIV iv)+ putLazyByteString encryptedblock+putSKAddendum (SUSym symenc iv encryptedblock) =+ Right $ do+ put symenc+ putByteString (unIV iv)+ putLazyByteString encryptedblock+putSKAddendum (SUUnencrypted sk checksum) =+ do+ putSecret <- putSKey sk+ Right $ do+ putWord8 0+ let skb = runPut putSecret+ putLazyByteString skb+ putWord16be+ ( if checksum == 0+ then checksum16Bytes (BL.toStrict skb)+ else checksum+ )++checksum16Bytes :: B.ByteString -> Word16+checksum16Bytes =+ B.foldl'+ ( \a b ->+ fromIntegral+ ((fromIntegral a + fromIntegral b) `mod` (65536 :: Integer))+ )+ 0++putSKAddendumForPKPayload :: SomePKPayload -> SKAddendum -> Put+putSKAddendumForPKPayload pkp ska =+ case fromSKAddendumForPKPayload pkp ska of+ Left e -> error e+ Right (SomeSKAddendumV skaV) ->+ putSKAddendumForPKPayloadTyped pkp skaV++putSKAddendumForPKPayloadTyped+ :: SomePKPayload+ -> SKAddendumV v+ -> Put+putSKAddendumForPKPayloadTyped pkp (SKAUnencryptedLegacy sk checksum) = do+ putWord8 0+ let putSecret =+ case putSKeyForPKPayload pkp sk of+ Left err -> error err+ Right p -> p+ skb = runPut putSecret+ putLazyByteString skb+ putWord16be+ ( if checksum == 0+ then+ BL.foldl+ (\a b -> mod (a + fromIntegral b) 0xffff)+ (0 :: Word16)+ skb+ else checksum+ )+putSKAddendumForPKPayloadTyped pkp (SKAUnencryptedV6 sk) = do+ putWord8 0+ let putSecret =+ case putSKeyForPKPayload pkp sk of+ Left err -> error err+ Right p -> p+ skb = runPut putSecret+ putLazyByteString skb+putSKAddendumForPKPayloadTyped _ (SKASHA1V6 symenc s2k iv encryptedblock) = do+ let s2kbs = runPut (put s2k)+ paramsLen = 1 + 1 + BL.length s2kbs + fromIntegral (B.length (unIV iv))+ putWord8 254+ putWord8 (fromIntegral paramsLen)+ put symenc+ putWord8 (fromIntegral (BL.length s2kbs))+ putLazyByteString s2kbs+ putByteString (unIV iv)+ putLazyByteString encryptedblock+putSKAddendumForPKPayloadTyped _ (SKAAEADV6 symenc aead s2k iv encryptedblock) = do+ let s2kbs = runPut (put s2k)+ paramsLen =+ 1 + 1 + 1 + BL.length s2kbs + fromIntegral (B.length (unIV iv))+ putWord8 253+ putWord8 (fromIntegral paramsLen)+ put symenc+ putWord8 (fromFVal aead)+ putWord8 (fromIntegral (BL.length s2kbs))+ putLazyByteString s2kbs+ putByteString (unIV iv)+ putLazyByteString encryptedblock+putSKAddendumForPKPayloadTyped _ (SKAAEADLegacy symenc aead s2k iv encryptedblock) = do+ putWord8 253+ put symenc+ putWord8 (fromFVal aead)+ put s2k+ putByteString (unIV iv)+ putLazyByteString encryptedblock+putSKAddendumForPKPayloadTyped _ (SKASymV6 symenc iv encryptedblock) = do+ putWord8 (fromFVal symenc)+ putWord8 (fromIntegral (B.length (unIV iv)))+ putByteString (unIV iv)+ putLazyByteString encryptedblock+putSKAddendumForPKPayloadTyped _ skaV =+ case putSKAddendum (toSKAddendum skaV) of+ Left e -> error e+ Right p -> p++aeadNonceSize :: AEADAlgorithm -> Int+aeadNonceSize EAX = 16+aeadNonceSize OCB = 15+aeadNonceSize GCM = 12+aeadNonceSize (OtherAEADAlgo _) = 0++symEncBlockSize :: SymmetricAlgorithm -> Either String Int+symEncBlockSize Plaintext = Right 0+symEncBlockSize IDEA = Right 8+symEncBlockSize TripleDES = Right 8+symEncBlockSize CAST5 = Right 8+symEncBlockSize Blowfish = Right 8+symEncBlockSize AES128 = Right 16+symEncBlockSize AES192 = Right 16+symEncBlockSize AES256 = Right 16+symEncBlockSize Twofish = Right 16+symEncBlockSize Camellia128 = Right 16+symEncBlockSize Camellia192 = Right 16+symEncBlockSize Camellia256 = Right 16+symEncBlockSize sa =+ Left+ ( "unsupported symmetric algorithm for secret-key IV sizing: "+ ++ show sa+ )++decodeIterationCount :: Word8 -> IterationCount+decodeIterationCount c =+ IterationCount+ ( (16 + (fromIntegral c .&. 15))+ `shiftL` ((fromIntegral c `shiftR` 4) + 6)+ )++encodeIterationCount :: IterationCount -> Word8 -- should this really be a lookup table?+encodeIterationCount 1024 = 0+encodeIterationCount 1088 = 1+encodeIterationCount 1152 = 2+encodeIterationCount 1216 = 3+encodeIterationCount 1280 = 4+encodeIterationCount 1344 = 5+encodeIterationCount 1408 = 6+encodeIterationCount 1472 = 7+encodeIterationCount 1536 = 8+encodeIterationCount 1600 = 9+encodeIterationCount 1664 = 10+encodeIterationCount 1728 = 11+encodeIterationCount 1792 = 12+encodeIterationCount 1856 = 13+encodeIterationCount 1920 = 14+encodeIterationCount 1984 = 15+encodeIterationCount 2048 = 16+encodeIterationCount 2176 = 17+encodeIterationCount 2304 = 18+encodeIterationCount 2432 = 19+encodeIterationCount 2560 = 20+encodeIterationCount 2688 = 21+encodeIterationCount 2816 = 22+encodeIterationCount 2944 = 23+encodeIterationCount 3072 = 24+encodeIterationCount 3200 = 25+encodeIterationCount 3328 = 26+encodeIterationCount 3456 = 27+encodeIterationCount 3584 = 28+encodeIterationCount 3712 = 29+encodeIterationCount 3840 = 30+encodeIterationCount 3968 = 31+encodeIterationCount 4096 = 32+encodeIterationCount 4352 = 33+encodeIterationCount 4608 = 34+encodeIterationCount 4864 = 35+encodeIterationCount 5120 = 36+encodeIterationCount 5376 = 37+encodeIterationCount 5632 = 38+encodeIterationCount 5888 = 39+encodeIterationCount 6144 = 40+encodeIterationCount 6400 = 41+encodeIterationCount 6656 = 42+encodeIterationCount 6912 = 43+encodeIterationCount 7168 = 44+encodeIterationCount 7424 = 45+encodeIterationCount 7680 = 46+encodeIterationCount 7936 = 47+encodeIterationCount 8192 = 48+encodeIterationCount 8704 = 49+encodeIterationCount 9216 = 50+encodeIterationCount 9728 = 51+encodeIterationCount 10240 = 52+encodeIterationCount 10752 = 53+encodeIterationCount 11264 = 54+encodeIterationCount 11776 = 55+encodeIterationCount 12288 = 56+encodeIterationCount 12800 = 57+encodeIterationCount 13312 = 58+encodeIterationCount 13824 = 59+encodeIterationCount 14336 = 60+encodeIterationCount 14848 = 61+encodeIterationCount 15360 = 62+encodeIterationCount 15872 = 63+encodeIterationCount 16384 = 64+encodeIterationCount 17408 = 65+encodeIterationCount 18432 = 66+encodeIterationCount 19456 = 67+encodeIterationCount 20480 = 68+encodeIterationCount 21504 = 69+encodeIterationCount 22528 = 70+encodeIterationCount 23552 = 71+encodeIterationCount 24576 = 72+encodeIterationCount 25600 = 73+encodeIterationCount 26624 = 74+encodeIterationCount 27648 = 75+encodeIterationCount 28672 = 76+encodeIterationCount 29696 = 77+encodeIterationCount 30720 = 78+encodeIterationCount 31744 = 79+encodeIterationCount 32768 = 80+encodeIterationCount 34816 = 81+encodeIterationCount 36864 = 82+encodeIterationCount 38912 = 83+encodeIterationCount 40960 = 84+encodeIterationCount 43008 = 85+encodeIterationCount 45056 = 86+encodeIterationCount 47104 = 87+encodeIterationCount 49152 = 88+encodeIterationCount 51200 = 89+encodeIterationCount 53248 = 90+encodeIterationCount 55296 = 91+encodeIterationCount 57344 = 92+encodeIterationCount 59392 = 93+encodeIterationCount 61440 = 94+encodeIterationCount 63488 = 95+encodeIterationCount 65536 = 96+encodeIterationCount 69632 = 97+encodeIterationCount 73728 = 98+encodeIterationCount 77824 = 99+encodeIterationCount 81920 = 100+encodeIterationCount 86016 = 101+encodeIterationCount 90112 = 102+encodeIterationCount 94208 = 103+encodeIterationCount 98304 = 104+encodeIterationCount 102400 = 105+encodeIterationCount 106496 = 106+encodeIterationCount 110592 = 107+encodeIterationCount 114688 = 108+encodeIterationCount 118784 = 109+encodeIterationCount 122880 = 110+encodeIterationCount 126976 = 111+encodeIterationCount 131072 = 112+encodeIterationCount 139264 = 113+encodeIterationCount 147456 = 114+encodeIterationCount 155648 = 115+encodeIterationCount 163840 = 116+encodeIterationCount 172032 = 117+encodeIterationCount 180224 = 118+encodeIterationCount 188416 = 119+encodeIterationCount 196608 = 120+encodeIterationCount 204800 = 121+encodeIterationCount 212992 = 122+encodeIterationCount 221184 = 123+encodeIterationCount 229376 = 124+encodeIterationCount 237568 = 125+encodeIterationCount 245760 = 126+encodeIterationCount 253952 = 127+encodeIterationCount 262144 = 128+encodeIterationCount 278528 = 129+encodeIterationCount 294912 = 130+encodeIterationCount 311296 = 131+encodeIterationCount 327680 = 132+encodeIterationCount 344064 = 133+encodeIterationCount 360448 = 134+encodeIterationCount 376832 = 135+encodeIterationCount 393216 = 136+encodeIterationCount 409600 = 137+encodeIterationCount 425984 = 138+encodeIterationCount 442368 = 139+encodeIterationCount 458752 = 140+encodeIterationCount 475136 = 141+encodeIterationCount 491520 = 142+encodeIterationCount 507904 = 143+encodeIterationCount 524288 = 144+encodeIterationCount 557056 = 145+encodeIterationCount 589824 = 146+encodeIterationCount 622592 = 147+encodeIterationCount 655360 = 148+encodeIterationCount 688128 = 149+encodeIterationCount 720896 = 150+encodeIterationCount 753664 = 151+encodeIterationCount 786432 = 152+encodeIterationCount 819200 = 153+encodeIterationCount 851968 = 154+encodeIterationCount 884736 = 155+encodeIterationCount 917504 = 156+encodeIterationCount 950272 = 157+encodeIterationCount 983040 = 158+encodeIterationCount 1015808 = 159+encodeIterationCount 1048576 = 160+encodeIterationCount 1114112 = 161+encodeIterationCount 1179648 = 162+encodeIterationCount 1245184 = 163+encodeIterationCount 1310720 = 164+encodeIterationCount 1376256 = 165+encodeIterationCount 1441792 = 166+encodeIterationCount 1507328 = 167+encodeIterationCount 1572864 = 168+encodeIterationCount 1638400 = 169+encodeIterationCount 1703936 = 170+encodeIterationCount 1769472 = 171+encodeIterationCount 1835008 = 172+encodeIterationCount 1900544 = 173+encodeIterationCount 1966080 = 174+encodeIterationCount 2031616 = 175+encodeIterationCount 2097152 = 176+encodeIterationCount 2228224 = 177+encodeIterationCount 2359296 = 178+encodeIterationCount 2490368 = 179+encodeIterationCount 2621440 = 180+encodeIterationCount 2752512 = 181+encodeIterationCount 2883584 = 182+encodeIterationCount 3014656 = 183+encodeIterationCount 3145728 = 184+encodeIterationCount 3276800 = 185+encodeIterationCount 3407872 = 186+encodeIterationCount 3538944 = 187+encodeIterationCount 3670016 = 188+encodeIterationCount 3801088 = 189+encodeIterationCount 3932160 = 190+encodeIterationCount 4063232 = 191+encodeIterationCount 4194304 = 192+encodeIterationCount 4456448 = 193+encodeIterationCount 4718592 = 194+encodeIterationCount 4980736 = 195+encodeIterationCount 5242880 = 196+encodeIterationCount 5505024 = 197+encodeIterationCount 5767168 = 198+encodeIterationCount 6029312 = 199+encodeIterationCount 6291456 = 200+encodeIterationCount 6553600 = 201+encodeIterationCount 6815744 = 202+encodeIterationCount 7077888 = 203+encodeIterationCount 7340032 = 204+encodeIterationCount 7602176 = 205+encodeIterationCount 7864320 = 206+encodeIterationCount 8126464 = 207+encodeIterationCount 8388608 = 208+encodeIterationCount 8912896 = 209+encodeIterationCount 9437184 = 210+encodeIterationCount 9961472 = 211+encodeIterationCount 10485760 = 212+encodeIterationCount 11010048 = 213+encodeIterationCount 11534336 = 214+encodeIterationCount 12058624 = 215+encodeIterationCount 12582912 = 216+encodeIterationCount 13107200 = 217+encodeIterationCount 13631488 = 218+encodeIterationCount 14155776 = 219+encodeIterationCount 14680064 = 220+encodeIterationCount 15204352 = 221+encodeIterationCount 15728640 = 222+encodeIterationCount 16252928 = 223+encodeIterationCount 16777216 = 224+encodeIterationCount 17825792 = 225+encodeIterationCount 18874368 = 226+encodeIterationCount 19922944 = 227+encodeIterationCount 20971520 = 228+encodeIterationCount 22020096 = 229+encodeIterationCount 23068672 = 230+encodeIterationCount 24117248 = 231+encodeIterationCount 25165824 = 232+encodeIterationCount 26214400 = 233+encodeIterationCount 27262976 = 234+encodeIterationCount 28311552 = 235+encodeIterationCount 29360128 = 236+encodeIterationCount 30408704 = 237+encodeIterationCount 31457280 = 238+encodeIterationCount 32505856 = 239+encodeIterationCount 33554432 = 240+encodeIterationCount 35651584 = 241+encodeIterationCount 37748736 = 242+encodeIterationCount 39845888 = 243+encodeIterationCount 41943040 = 244+encodeIterationCount 44040192 = 245+encodeIterationCount 46137344 = 246+encodeIterationCount 48234496 = 247+encodeIterationCount 50331648 = 248+encodeIterationCount 52428800 = 249+encodeIterationCount 54525952 = 250+encodeIterationCount 56623104 = 251+encodeIterationCount 58720256 = 252+encodeIterationCount 60817408 = 253+encodeIterationCount 62914560 = 254+encodeIterationCount 65011712 = 255+encodeIterationCount n = error ("invalid iteration count" ++ show n)++getSignaturePayload :: Get SignaturePayload+getSignaturePayload = do+ pv <- getWord8+ case pv of+ 3 -> do+ hashlen <- getWord8+ guard (hashlen == 5)+ st <- getWord8+ ctime <- fmap ThirtyTwoBitTimeStamp getWord32be+ eok <- getLazyByteString 8+ pka <- get+ ha <- get+ left16 <- getWord16be+ mpib <- getRemainingLazyByteString+ case runGetOrFail (some getMPI) mpib of+ Left (_, _, e) -> fail ("v3 sig MPIs " ++ e)+ Right (_, _, mpis) ->+ return $+ SigV3+ (toFVal st)+ ctime+ (EightOctetKeyId eok)+ (toFVal pka)+ (toFVal ha)+ left16+ (NE.fromList mpis)+ 4 -> do+ st <- getWord8+ pkaOctet <- get+ ha <- get+ let pka = toFVal pkaOctet :: PubKeyAlgorithm+ hlen <- getWord16be+ hb <- getLazyByteString (fromIntegral hlen)+ let hashed =+ case runGetOrFail (many getSigSubPacket) hb of+ Left (_, _, err) -> fail ("v4 sig hasheds " ++ err)+ Right (_, _, h) -> h+ ulen <- getWord16be+ ub <- getLazyByteString (fromIntegral ulen)+ let unhashed =+ case runGetOrFail (many getSigSubPacket) ub of+ Left (_, _, err) -> fail ("v4 sig unhasheds " ++ err)+ Right (_, _, u) -> u+ left16 <- getWord16be+ mpib <- getRemainingLazyByteString+ let parseV4MPIs parseErrPrefix =+ case runGetOrFail (some getMPI) mpib of+ Left (_, _, e) -> fail (parseErrPrefix ++ e)+ Right (_, _, mpis) ->+ return $+ SigV4+ (toFVal st)+ pka+ (toFVal ha)+ hashed+ unhashed+ left16+ (NE.fromList mpis)+ if pka == BTypes.Ed25519+ then+ if BL.length mpib == 64+ then do+ let sig = BL.toStrict mpib+ (rbs, sbs) = B.splitAt 32 sig+ return $+ SigV4+ (toFVal st)+ pka+ (toFVal ha)+ hashed+ unhashed+ left16+ (NE.fromList [MPI (os2ip rbs), MPI (os2ip sbs)])+ else parseV4MPIs "v4 Ed25519 legacy MPIs "+ else+ if pka == BTypes.Ed448+ then+ if BL.length mpib == 114+ then do+ let sig = BL.toStrict mpib+ (rbs, sbs) = B.splitAt 57 sig+ return $+ SigV4+ (toFVal st)+ pka+ (toFVal ha)+ hashed+ unhashed+ left16+ (NE.fromList [MPI (os2ip rbs), MPI (os2ip sbs)])+ else parseV4MPIs "v4 Ed448 legacy MPIs "+ else parseV4MPIs "v4 sig MPIs "+ 6 -> do+ st <- getWord8+ pka <- get+ ha <- get+ hlen <- getWord32be+ hb <- getLazyByteString (fromIntegral hlen)+ let hashed =+ case runGetOrFail (many getSigSubPacket) hb of+ Left (_, _, err) -> fail ("v6 sig hasheds " ++ err)+ Right (_, _, h) -> h+ ulen <- getWord32be+ ub <- getLazyByteString (fromIntegral ulen)+ let unhashed =+ case runGetOrFail (many getSigSubPacket) ub of+ Left (_, _, err) -> fail ("v6 sig unhasheds " ++ err)+ Right (_, _, u) -> u+ left16 <- getWord16be+ saltSize <- getWord8+ let haVal = (toFVal ha :: HashAlgorithm)+ expectedSaltSize <-+ maybe+ ( fail+ ( "signature hash algorithm does not define a V6 salt size: "+ ++ show haVal+ )+ )+ pure+ (v6SaltSizeForHashAlgorithm haVal)+ when (saltSize /= expectedSaltSize) $+ fail+ ( "v6 signature salt size mismatch for "+ ++ show haVal+ ++ ": expected "+ ++ show expectedSaltSize+ ++ ", got "+ ++ show saltSize+ )+ saltbs <- getByteString (fromIntegral saltSize)+ let salt = SignatureSalt (BL.fromStrict saltbs)+ if pka == BTypes.Ed25519+ then do+ sig <- getByteString 64+ let (rbs, sbs) = B.splitAt 32 sig+ mpis = [MPI (os2ip rbs), MPI (os2ip sbs)]+ return $+ SigV6+ (toFVal st)+ pka+ (toFVal ha)+ salt+ hashed+ unhashed+ left16+ (NE.fromList mpis)+ else+ if pka == BTypes.Ed448+ then do+ sig <- getByteString 114+ let (rbs, sbs) = B.splitAt 57 sig+ mpis = [MPI (os2ip rbs), MPI (os2ip sbs)]+ return $+ SigV6+ (toFVal st)+ pka+ (toFVal ha)+ salt+ hashed+ unhashed+ left16+ (NE.fromList mpis)+ else do+ mpib <- getRemainingLazyByteString+ case runGetOrFail (some getMPI) mpib of+ Left (_, _, e) -> fail ("v6 sig MPIs " ++ e)+ Right (_, _, mpis) ->+ return $+ SigV6+ (toFVal st)+ pka+ (toFVal ha)+ salt+ hashed+ unhashed+ left16+ (NE.fromList mpis)+ _ -> do+ bs <- getRemainingLazyByteString+ return $ SigVOther pv bs++putSignaturePayload :: SignaturePayload -> Put+putSignaturePayload (SigV3 st ctime eok pka ha left16 mpis) = do+ putWord8 3+ putWord8 5 -- hashlen+ put st+ putWord32be . unThirtyTwoBitTimeStamp $ ctime+ putLazyByteString (unEOKI eok)+ put pka+ put ha+ putWord16be left16+ F.mapM_ put mpis+putSignaturePayload (SigV4 st pka ha hashed unhashed left16 mpis) = do+ putWord8 4+ put st+ put pka+ put ha+ let hb = runPut $ mapM_ put hashed+ putWord16be . fromIntegral . BL.length $ hb+ putLazyByteString hb+ let ub = runPut $ mapM_ put unhashed+ putWord16be . fromIntegral . BL.length $ ub+ putLazyByteString ub+ putWord16be left16+ if pka == BTypes.Ed25519+ then case NE.toList mpis of+ [MPI r, MPI s] -> do+ putByteString (padN 32 r)+ putByteString (padN 32 s)+ _ -> error "Ed25519 v4 signatures must have two MPIs"+ else+ if pka == BTypes.Ed448+ then case NE.toList mpis of+ [MPI r, MPI s] -> do+ putByteString (padN 57 r)+ putByteString (padN 57 s)+ _ -> error "Ed448 v4 signatures must have two MPIs"+ else F.mapM_ put mpis+ where+ padN n i =+ let bs = i2osp i+ in B.replicate (max 0 (n - B.length bs)) 0 <> bs+putSignaturePayload (SigV6 st pka ha salt hashed unhashed left16 mpis) = do+ let expectedSaltSize =+ maybe+ ( error+ ( "signature hash algorithm does not define a V6 salt size: "+ ++ show ha+ )+ )+ id+ (v6SaltSizeForHashAlgorithm ha)+ actualSaltSize = fromIntegral (BL.length (unSignatureSalt salt))+ when (actualSaltSize /= expectedSaltSize) $+ error+ ( "v6 signature salt size mismatch for "+ ++ show ha+ ++ ": expected "+ ++ show expectedSaltSize+ ++ ", got "+ ++ show actualSaltSize+ )+ putWord8 6+ put st+ put pka+ put ha+ let hb = runPut $ mapM_ put hashed+ putWord32be . fromIntegral . BL.length $ hb+ putLazyByteString hb+ let ub = runPut $ mapM_ put unhashed+ putWord32be . fromIntegral . BL.length $ ub+ putLazyByteString ub+ putWord16be left16+ putWord8 . fromIntegral . BL.length . unSignatureSalt $ salt+ putByteString (BL.toStrict (unSignatureSalt salt))+ if pka == BTypes.Ed25519+ then case NE.toList mpis of+ [MPI r, MPI s] -> do+ putByteString (padN 32 r)+ putByteString (padN 32 s)+ _ -> error "Ed25519 v6 signatures must have two MPIs"+ else+ if pka == BTypes.Ed448+ then case NE.toList mpis of+ [MPI r, MPI s] -> do+ putByteString (padN 57 r)+ putByteString (padN 57 s)+ _ -> error "Ed448 v6 signatures must have two MPIs"+ else F.mapM_ put mpis+ where+ padN n i =+ let bs = i2osp i+ in B.replicate (max 0 (n - B.length bs)) 0 <> bs+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_ 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]+parsePkts = reverse . fst . parsePktsAccum 0 []++-- | Parse packets from a ByteString and report the first parse failure.+parsePktsEither :: ByteString -> Either PktParseError [Pkt]+parsePktsEither lbs =+ case parsePktsAccum 0 [] lbs of+ (pkts, Nothing) -> Right (reverse pkts)+ (_, Just err) -> Left err++data PktParseError+ = PktParseError+ { pktParseErrorOffset :: Int64+ , pktParseErrorMessage :: String+ }+ deriving (Eq, Show)++parsePktsAccum+ :: Int64 -> [Pkt] -> ByteString -> ([Pkt], Maybe PktParseError)+parsePktsAccum offset acc lbs+ | BL.null lbs = (acc, Nothing)+ | otherwise =+ case runGetOrFail getPkt lbs of+ Left (_, parseOffset, msg) -> (acc, err parseOffset msg)+ Right (rest, consumed, pkt) ->+ parsePktsAccum (offset + consumed) (pkt : acc) rest+ where+ err parseOffset msg =+ Just+ PktParseError+ { pktParseErrorOffset = offset + parseOffset+ , pktParseErrorMessage = msg+ }++armorPayloads :: [Armor] -> [ByteString]+armorPayloads =+ foldr collect []+ where+ collect (Armor _ _ payload) = (BL.fromStrict (BLC8.toStrict payload) :)+ collect (ClearSigned _ _ inner) = (armorPayloads [inner] ++)++looksLikeAsciiArmor :: ByteString -> Bool+looksLikeAsciiArmor =+ (armorHeaderLazy `BL.isPrefixOf`)+ . BL.dropWhile isLeadingArmorWhitespace++looksLikeAsciiArmorLenient :: ByteString -> Bool+looksLikeAsciiArmorLenient = looksLikeAsciiArmor . stripUtf8Bom++armorPayloadsOfType :: ArmorType -> [Armor] -> [ByteString]+armorPayloadsOfType atype =+ foldr collect []+ where+ collect (Armor innerType _ payload)+ | innerType == atype = (BL.fromStrict (BLC8.toStrict payload) :)+ | otherwise = id+ collect (ClearSigned _ _ inner) = (armorPayloadsOfType atype [inner] ++)++singleArmorPayloadOfType+ :: ArmorType -> [Armor] -> Either String ByteString+singleArmorPayloadOfType atype armors =+ case armorPayloadsOfType atype armors of+ [payload] -> Right payload+ [] ->+ Left+ ("ASCII armor decode returned no " ++ show atype ++ " blocks")+ payloads ->+ Left+ ( "ASCII armor decode returned "+ ++ show (length payloads)+ ++ " "+ ++ show atype+ ++ " blocks (expected exactly one)"+ )++singleClearSignedBlock+ :: [Armor]+ -> Either String ([(String, String)], ByteString, ByteString)+singleClearSignedBlock armors =+ case [ clearSignedBlockFromArmor armor | armor@ClearSigned {} <- armors+ ] of+ [Right clearSigned] -> Right clearSigned+ [Left err] -> Left err+ [] -> Left "ASCII armor decode returned no clear-signed blocks"+ clearSigneds ->+ Left+ ( "ASCII armor decode returned "+ ++ show (length clearSigneds)+ ++ " clear-signed blocks (expected exactly one)"+ )+ where+ clearSignedBlockFromArmor (ClearSigned hs cleartext inner) =+ case inner of+ Armor ArmorSignature _ sig ->+ Right+ ( hs+ , BL.fromStrict (BLC8.toStrict cleartext)+ , BL.fromStrict (BLC8.toStrict sig)+ )+ Armor atype _ _ ->+ Left+ ( "clear-signed block contained inner armor type "+ ++ show atype+ ++ " (expected ArmorSignature)"+ )+ ClearSigned {} ->+ Left "clear-signed block contained nested clear-signed payload"+ clearSignedBlockFromArmor _ =+ Left "internal error: expected ClearSigned armor block"++recommendedArmorType :: [Pkt] -> Maybe ArmorType+recommendedArmorType [] = Nothing+recommendedArmorType (pkt : _)+ | isPrivateKeyPacket pkt = Just ArmorPrivateKeyBlock+ | isPublicKeyPacket pkt = Just ArmorPublicKeyBlock+ | isSignaturePacket pkt = Just ArmorSignature+ | otherwise = Just ArmorMessage+ where+ isPrivateKeyPacket SecretKeyPkt {} = True+ isPrivateKeyPacket SecretSubkeyPkt {} = True+ isPrivateKeyPacket _ = False++ isPublicKeyPacket PublicKeyPkt {} = True+ isPublicKeyPacket PublicSubkeyPkt {} = True+ isPublicKeyPacket _ = False++ isSignaturePacket SignaturePkt {} = True+ isSignaturePacket _ = False++dearmorIfAsciiArmored+ :: ByteString -> Either String (Bool, ByteString)+dearmorIfAsciiArmored bs+ | looksLikeAsciiArmor bs =+ (\payload -> (True, payload)) <$> decodeSingleArmorPayload bs+ | otherwise = Right (False, bs)++dearmorIfAsciiArmoredLenient+ :: ByteString -> Either String (Bool, ByteString)+dearmorIfAsciiArmoredLenient bs+ | looksLikeAsciiArmorLenient bs =+ (\payload -> (True, payload))+ <$> decodeSingleArmorPayloadLenient (stripUtf8Bom bs)+ | otherwise = Right (False, bs)++decodeSingleArmorPayload+ :: ByteString -> Either String ByteString+decodeSingleArmorPayload bs =+ case AA.decodeLazy bs of+ Left err -> Left err+ Right armors -> singleArmorPayload armors++decodeSingleArmorPayloadLenient+ :: ByteString -> Either String ByteString+decodeSingleArmorPayloadLenient bs =+ case decodeSingleArmorPayload bs of+ Right payload -> Right payload+ Left strictErr ->+ let normalized = normalizeAsciiArmorForLenientDecode bs+ in if normalized == bs+ then Left strictErr+ else case decodeSingleArmorPayload normalized of+ Left lenientErr ->+ Left+ ( strictErr+ ++ " (lenient normalization retry failed: "+ ++ lenientErr+ ++ ")"+ )+ Right payload -> Right payload++singleArmorPayload :: [Armor] -> Either String ByteString+singleArmorPayload armors =+ case armorPayloads armors of+ [payload] -> Right payload+ [] -> Left "ASCII armor decode succeeded but returned no blocks"+ payloads ->+ Left+ ( "ASCII armor decode returned "+ ++ show (length payloads)+ ++ " blocks (expected exactly one)"+ )++data WireRepInput+ = WireRepInput+ { wireRepInputRef :: WireRepRef+ , wireRepInputPayload :: ByteString+ }++wireRepRefFromInput+ :: Maybe T.Text -> ByteString -> Either String WireRepInput+wireRepRefFromInput mname bs =+ ( \(wasArmored, payload) ->+ WireRepInput+ { wireRepInputRef = BTypes.mkWireRepRef mname wasArmored payload+ , wireRepInputPayload = payload+ }+ )+ <$> dearmorIfAsciiArmored bs++data ParseState+ = ParseState+ { psOffset :: Int64+ , psIndex :: Int+ , psSource :: WireRepRef+ , psRemaining :: BL.ByteString+ }++-- | Parse packets from a source bytestream, preserving packet provenance.+parsePktsWithWireRep+ :: WireRepRef -> ByteString -> [PktWithWireRep]+parsePktsWithWireRep src input = go initialState+ where+ initialState =+ ParseState+ { psOffset = 0+ , psIndex = 0+ , psSource = src+ , psRemaining = input+ }+ go state+ | BL.null (psRemaining state) = []+ | otherwise =+ case runGetOrFail getPkt (psRemaining state) of+ Left (_, _, _) -> []+ Right (rest, consumed, pkt) ->+ let raw = BL.take consumed (psRemaining state)+ newState =+ state+ { psOffset = psOffset state + consumed+ , psIndex = psIndex state + 1+ , psRemaining = rest+ }+ pktWithSource =+ PktWithWireRep+ (psSource state)+ (ByteRange (psOffset state) consumed)+ raw+ (psIndex state)+ pkt+ in pktWithSource : go newState++conduitParsePktsWithWireRep+ :: (Monad m)+ => Maybe T.Text -> ConduitT B.ByteString PktWithWireRep m ()+conduitParsePktsWithWireRep mname = go (UndecidedInput [])+ where+ go !state = do+ mchunk <- await+ case mchunk of+ Nothing -> mapM_ yield (finishConduitState mname state)+ Just chunk ->+ let !nextState = consumeConduitChunk chunk state+ in go nextState++data ConduitParseState+ = UndecidedInput ![B.ByteString]+ | ArmoredInput ![B.ByteString]+ | BinaryInput !BinaryParseState++data BinaryParseState+ = BinaryParseState+ { bpsLength :: !Int64+ , bpsOffset :: !Int64+ , bpsIndex :: !Int+ , bpsBuffer :: !B.ByteString+ , bpsParsedRev :: [ParsedPacketChunk]+ }++data ArmorPrefixDecision+ = PrefixNeedsMore+ | PrefixIsArmored+ | PrefixIsBinary++consumeConduitChunk+ :: B.ByteString -> ConduitParseState -> ConduitParseState+consumeConduitChunk chunk (UndecidedInput chunksRev) =+ let prefixChunksRev = chunk : chunksRev+ prefix = B.concat (reverse prefixChunksRev)+ in case classifyArmorPrefix prefix of+ PrefixNeedsMore -> UndecidedInput prefixChunksRev+ PrefixIsArmored -> ArmoredInput prefixChunksRev+ PrefixIsBinary -> feedBinaryChunk prefix initialBinaryParseState+consumeConduitChunk chunk (ArmoredInput chunksRev) = ArmoredInput (chunk : chunksRev)+consumeConduitChunk chunk (BinaryInput state) = BinaryInput (advanceBinaryParseState chunk state)++finishConduitState+ :: Maybe T.Text -> ConduitParseState -> [PktWithWireRep]+finishConduitState mname (UndecidedInput chunksRev) =+ finalizeBinaryParseState+ mname+ ( advanceBinaryParseState+ (B.concat (reverse chunksRev))+ initialBinaryParseState+ )+finishConduitState mname (ArmoredInput chunksRev) =+ let input = BL.fromChunks (reverse chunksRev)+ in case wireRepRefFromInput mname input of+ Left _ -> []+ Right+ WireRepInput+ { wireRepInputRef = src+ , wireRepInputPayload = payload+ } ->+ parsePktsWithWireRep src payload+finishConduitState mname (BinaryInput state) = finalizeBinaryParseState mname state++initialBinaryParseState :: BinaryParseState+initialBinaryParseState =+ BinaryParseState+ { bpsLength = 0+ , bpsOffset = 0+ , bpsIndex = 0+ , bpsBuffer = B.empty+ , bpsParsedRev = []+ }++feedBinaryChunk+ :: B.ByteString -> BinaryParseState -> ConduitParseState+feedBinaryChunk chunk = BinaryInput . advanceBinaryParseState chunk++advanceBinaryParseState+ :: B.ByteString -> BinaryParseState -> BinaryParseState+advanceBinaryParseState chunk state =+ let !nextLength = bpsLength state + fromIntegral (B.length chunk)+ !(nextOffset, nextIndex, nextBuffer, nextParsedRev) =+ drainParsedPackets+ (bpsOffset state)+ (bpsIndex state)+ (bpsBuffer state <> chunk)+ (bpsParsedRev state)+ in BinaryParseState+ { bpsLength = nextLength+ , bpsOffset = nextOffset+ , bpsIndex = nextIndex+ , bpsBuffer = nextBuffer+ , bpsParsedRev = nextParsedRev+ }++finalizeBinaryParseState+ :: Maybe T.Text -> BinaryParseState -> [PktWithWireRep]+finalizeBinaryParseState mname state =+ let src = BTypes.mkWireRepRefWithLength mname False (bpsLength state)+ in map (toPktWithWireRep src) (reverse (bpsParsedRev state))++classifyArmorPrefix :: B.ByteString -> ArmorPrefixDecision+classifyArmorPrefix prefix =+ case B.dropWhile isLeadingArmorWhitespace prefix of+ rest+ | B.null rest -> PrefixNeedsMore+ | armorHeader `B.isPrefixOf` rest -> PrefixIsArmored+ | rest `B.isPrefixOf` armorHeader -> PrefixNeedsMore+ | otherwise -> PrefixIsBinary++armorHeader :: B.ByteString+armorHeader = B.pack (map (fromIntegral . fromEnum) "-----BEGIN PGP ")++armorHeaderLazy :: ByteString+armorHeaderLazy = BL.fromStrict armorHeader++utf8Bom :: ByteString+utf8Bom = BL.pack [0xef, 0xbb, 0xbf]++isLeadingArmorWhitespace :: Word8 -> Bool+isLeadingArmorWhitespace w = w == 0x20 || w == 0x09 || w == 0x0d || w == 0x0a++stripUtf8Bom :: ByteString -> ByteString+stripUtf8Bom bs+ | utf8Bom `BL.isPrefixOf` bs =+ BL.drop (fromIntegral (BL.length utf8Bom)) bs+ | otherwise = bs++normalizeAsciiArmorForLenientDecode :: ByteString -> ByteString+normalizeAsciiArmorForLenientDecode =+ ensureTrailingLf . normalizeLineEndings+ where+ ensureTrailingLf lbs+ | BL.null lbs = lbs+ | BL.last lbs == 0x0a = lbs+ | otherwise = lbs <> BL.singleton 0x0a+ normalizeLineEndings lbs =+ case BL.uncons lbs of+ Nothing -> BL.empty+ Just (0x0d, rest) ->+ case BL.uncons rest of+ Just (0x0a, rest') -> BL.cons 0x0a (normalizeLineEndings rest')+ _ -> BL.cons 0x0a (normalizeLineEndings rest)+ Just (w, rest) -> BL.cons w (normalizeLineEndings rest)++data ParsedPacketChunk+ = ParsedPacketChunk+ { ppcRange :: ByteRange+ , ppcRaw :: ByteString+ , ppcIndex :: Int+ , ppcValue :: Pkt+ }++toPktWithWireRep+ :: WireRepRef -> ParsedPacketChunk -> PktWithWireRep+toPktWithWireRep src ppc =+ PktWithWireRep+ src+ (ppcRange ppc)+ (ppcRaw ppc)+ (ppcIndex ppc)+ (ppcValue ppc)++drainParsedPackets+ :: Int64+ -> Int+ -> B.ByteString+ -> [ParsedPacketChunk]+ -> (Int64, Int, B.ByteString, [ParsedPacketChunk])+drainParsedPackets !offset !idx !buffer acc+ | B.null buffer = (offset, idx, B.empty, acc)+ | otherwise =+ case runGetOrFail getPkt (BL.fromStrict buffer) of+ Left _ -> (offset, idx, buffer, acc)+ Right (rest, consumed, pkt) ->+ let !consumedLen = fromIntegral consumed+ !nextOffset = offset + consumed+ !nextIdx = idx + 1+ !nextBuffer = BL.toStrict rest+ parsedPacket =+ ParsedPacketChunk+ { ppcRange = ByteRange offset consumed+ , ppcRaw = BL.fromStrict (B.take consumedLen buffer)+ , ppcIndex = idx+ , ppcValue = pkt+ }+ in drainParsedPackets+ nextOffset+ nextIdx+ nextBuffer+ (parsedPacket : acc)
Codec/Encryption/OpenPGP/Signatures.hs view
@@ -1,1766 +1,2404 @@--- Signatures.hs: OpenPGP (RFC9580) signature verification--- Copyright © 2012-2026 Clint Adams--- This software is released under the terms of the Expat license.--- (See the LICENSE file).--{-# LANGUAGE DataKinds #-}-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE UndecidableInstances #-}--module Codec.Encryption.OpenPGP.Signatures- ( SignError(..)- , renderSignError- , CertificationState(..)- , certificationStateAt- , VerificationError(..)- , renderVerificationError- , verifySigWith- , verifyAgainstKeyring- , verifyAgainstKeys- , verifyTKWith- , verifyUnknownTKWith- , signCertificationWithRSA- , signDirectKeyWithRSA- , signKeyRevocationWithRSA- , signSubkeyRevocationWithRSA- , signCertRevocationWithRSA- , signUserIDwithRSA- , crossSignSubkeyWithRSA- , signDataWithEd25519- , signDataWithEd25519Legacy- , signDataWithEd25519V6- , signDataWithEd448- , signDataWithEd448V6- , signDataWithRSA- , signDataWithRSAV6- -- * Builder-based API (Phase 2)- , signDataWithRSABuilder- , signDataWithRSAV6Builder- , signDataWithEd25519Builder- , signDataWithEd25519V6Builder- , signDataWithEd448Builder- , signDataWithEd448V6Builder- , signDataWithAlgorithmicBuilder- -- * Text normalization mode- , TextNormalizationMode(..)- ) where--import Control.Applicative ((<|>))-import Control.Error.Util (hush)-import Control.Lens ((&), (^.), _1)-import Control.Monad (liftM2, when)--import Crypto.Error (eitherCryptoError)-import Crypto.Hash (hashWith)-import qualified Crypto.Hash.Algorithms as CHA-import Crypto.Number.Serialize (i2osp, os2ip)-import qualified Crypto.PubKey.DSA as DSA-import qualified Crypto.PubKey.ECC.ECDSA as ECDSA-import qualified Crypto.PubKey.Ed25519 as Ed25519-import qualified Crypto.PubKey.Ed448 as Ed448-import qualified Crypto.PubKey.RSA.PKCS15 as P15-import qualified Crypto.PubKey.RSA.Types as RSATypes--import Data.Bifunctor (first)-import Data.Binary.Put (runPut)-import qualified Data.ByteArray as BA-import qualified Data.ByteString as B-import Data.ByteString.Lazy (ByteString)-import qualified Data.ByteString.Lazy as BL-import Data.Either (isRight, lefts, rights)-import Data.Function (on)-import Data.IxSet.Typed ((@=))-import qualified Data.IxSet.Typed as IxSet-import qualified Data.Set as Set-import Data.List (find, intercalate, nub)-import Data.List.NonEmpty (NonEmpty(..))-import qualified Data.List.NonEmpty as NE-import qualified Data.Map.Strict as Map-import Data.Maybe (isJust, mapMaybe)-import Data.Text (Text)-import Data.Time.Clock (UTCTime(..), addUTCTime, diffUTCTime)-import Data.Time.Clock.POSIX (posixSecondsToUTCTime)-import Data.Word (Word16, Word8)-import GHC.TypeLits (TypeError, ErrorMessage(..))--import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)-import Codec.Encryption.OpenPGP.Expirations- ( isPKTimeValidWithSelfSignatures- , keyStateAt- , keyStateValid- )-import Codec.Encryption.OpenPGP.Internal- ( PktStreamContext(..)- , emptyPSC- , issuer- , issuerFP- )-import Codec.Encryption.OpenPGP.Ontology- ( isCertRevocationSig- , isRevocationKeySSP- , isRevokerP- , isSubkeyBindingSig- , isSubkeyRevocation- )-import Codec.Encryption.OpenPGP.SignatureQualities- ( sigCT- , sigHA- , sigPKA- , sigType- , signatureHashedSubpacketsKnown- , signatureSubpacketListsKnown- )--import Codec.Encryption.OpenPGP.SerializeForSigs- ( payloadForSig- , putKeyforSigning- , putPartialSigforSigning- , putSigTrailer- , putUforSigning- )-import Codec.Encryption.OpenPGP.Policy (signatureV6SaltSizeForHashAlgorithm)-import Codec.Encryption.OpenPGP.Subpackets- ( SigBuilder- , TextNormalizationMode(..)- , sbSigType- , sbHashAlgo- , sbHashedSubs- , sbUnhashedSubs- , sbSalt- , sbTextNormMode- , PrivateKeyFor- )-import qualified Codec.Encryption.OpenPGP.Subpackets as SP-import Codec.Encryption.OpenPGP.Types-import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as PKA-import Data.Conduit.OpenPGP.Keyring.Instances ()--data VerificationError- = IssuerSubpacketMismatch- | IssuerSubpacketUncheckable String- | IssuerKeyIdProhibitedInV6Signature- | IssuerFingerprintSubpacketMismatch- | UnsupportedCriticalSubpacket SigType- | UnknownCriticalPacketInStream Word8- | BrokenCriticalPacketInStream Word8 String- | ExternalVerificationError String- | NonSignaturePacket- | UnexpectedSignaturePayloadShape- | MissingHashAlgorithm- | HashComputationFailed String- | UnexpectedKeyVersion- | SignatureHashUnsupportedByAlgorithm HashAlgorithm PubKeyAlgorithm- | KeyRevoked- | SigningKeyUnavailableAtSignatureTime- | MissingIssuer- | SigningKeyNotFound (Maybe EightOctetKeyId) (Maybe Fingerprint)- | MultipleVerificationSuccesses Int- | UnsupportedKeyType PubKeyAlgorithm- | SignatureMismatch PubKeyAlgorithm Fingerprint- | SignatureShapeMismatch PubKeyAlgorithm- | SignatureEncodingInvalid PubKeyAlgorithm String- | SignaturePolicyHashUnsupported HashAlgorithm- | SignaturePolicyPKAMismatch PubKeyAlgorithm PubKeyAlgorithm- | SignatureExpired- | CandidateKeyFailures [VerificationError]- | InvalidSubkeyBackSignature VerificationError- -- ^ An embedded primary-key back-signature (type 0x19) in a subkey- -- binding signature failed to verify.- deriving (Eq, Show)--data CertificationState- = CertificationNotYetKnown- | CertificationActive- | CertificationRevoked- deriving (Eq, Show)--renderVerificationError :: VerificationError -> String-renderVerificationError IssuerSubpacketMismatch =- "verification failed: issuer subpacket does not match the actual signer"-renderVerificationError (IssuerSubpacketUncheckable err) =- "verification failed: issuer subpacket cannot be checked (" ++ err ++ ")"-renderVerificationError IssuerKeyIdProhibitedInV6Signature =- "verification failed: Issuer Key ID subpacket is prohibited in v6 signatures"-renderVerificationError IssuerFingerprintSubpacketMismatch =- "verification failed: issuer fingerprint subpacket does not match the actual signer"-renderVerificationError (UnsupportedCriticalSubpacket sigType) =- "verification failed: unsupported critical hashed subpacket in " ++- show sigType ++ " signature"-renderVerificationError (UnknownCriticalPacketInStream t) =- "verification failed: unknown critical packet type in packet sequence (" ++ show t ++ ")"-renderVerificationError (BrokenCriticalPacketInStream t err) =- "verification failed: broken critical packet type " ++ show t ++ ": " ++ err-renderVerificationError (ExternalVerificationError err) = err-renderVerificationError NonSignaturePacket =- "verification failed: non-signature packet encountered where signature was expected"-renderVerificationError UnexpectedSignaturePayloadShape =- "verification failed: unexpected signature payload shape"-renderVerificationError MissingHashAlgorithm =- "verification failed: signature payload is missing hash algorithm"-renderVerificationError (HashComputationFailed err) =- "verification failed: hash computation error (" ++ err ++ ")"-renderVerificationError UnexpectedKeyVersion =- "verification failed: signing key has unexpected version (only v4 and v6 are supported)"-renderVerificationError (SignatureHashUnsupportedByAlgorithm ha pka) =- "verification failed: hash algorithm " ++ show ha ++- " is not supported by " ++ show pka ++ " signing backend"-renderVerificationError KeyRevoked =- "verification failed: signing key is revoked"-renderVerificationError SigningKeyUnavailableAtSignatureTime =- "verification failed: signing key was not valid at the signature creation time"-renderVerificationError MissingIssuer =- "verification failed: signature is missing issuer information"-renderVerificationError (SigningKeyNotFound meoki mfp) =- "verification failed: signing key not found in keyring" ++ issuerContext meoki mfp-renderVerificationError (MultipleVerificationSuccesses n) =- "verification failed: multiple successful key matches (" ++ show n ++ ")"-renderVerificationError (UnsupportedKeyType pka) =- "verification failed: unsupported public key algorithm for verification (" ++ show pka ++ ")"-renderVerificationError (SignatureMismatch pka fpr) =- "verification failed: " ++ show pka ++ " signature mismatch (signer " ++ show fpr ++ ")"-renderVerificationError (SignatureShapeMismatch pka) =- "verification failed: malformed " ++ show pka ++ " signature encoding"-renderVerificationError (SignatureEncodingInvalid pka err) =- "verification failed: invalid " ++ show pka ++ " key/signature encoding (" ++ err ++ ")"-renderVerificationError (SignaturePolicyHashUnsupported ha) =- "verification failed: unsupported signature hash policy (" ++ show ha ++ ")"-renderVerificationError (SignaturePolicyPKAMismatch sigPka keyPka) =- "verification failed: signature public-key algorithm " ++- show sigPka ++ " does not match key algorithm " ++ show keyPka-renderVerificationError SignatureExpired =- "verification failed: signature expired"-renderVerificationError (CandidateKeyFailures errs) =- "verification failed: no candidate key validated the signature (" ++- intercalate "; " (nub (map renderVerificationError errs)) ++- ")"-renderVerificationError (InvalidSubkeyBackSignature err) =- "verification failed: embedded primary-key back-signature verification failed: " ++- renderVerificationError err--issuerContext ::- Maybe EightOctetKeyId -> Maybe Fingerprint -> String-issuerContext meoki mfp =- case (meoki, mfp) of- (Nothing, Nothing) -> ""- _ ->- " (issuer-keyid=" ++- maybe "unknown" show meoki ++- ", issuer-fingerprint=" ++- maybe "unknown" show mfp ++- ")"--verificationError :: VerificationError -> Either VerificationError a-verificationError = Left--renderVerificationResult :: Either VerificationError a -> Either String a-renderVerificationResult = first renderVerificationError--data SignError- = SignBackendError String- | SignUnsupportedCertificationType SigType- | SignUnsupportedKeySignatureType SigType- | SignV6SaltSizeMismatch HashAlgorithm Word8 Int- | SignProducedWrongLength String Int Int- deriving (Eq, Show)--renderSignError :: SignError -> String-renderSignError (SignBackendError err) =- "signature backend error: " ++ err-renderSignError (SignUnsupportedCertificationType st) =- "unsupported certification signature type: " ++- show st ++- " (expected one of GenericCert/PersonaCert/CasualCert/PositiveCert)"-renderSignError (SignUnsupportedKeySignatureType st) =- "unsupported key signature type: " ++- show st ++- " (expected SignatureDirectlyOnAKey or KeyRevocationSig)"-renderSignError (SignV6SaltSizeMismatch ha expected actual) =- "v6 signature salt size mismatch for " ++- show ha ++- ": expected " ++ show expected ++ ", got " ++ show actual-renderSignError (SignProducedWrongLength algo expected actual) =- algo ++ " produced a non-" ++ show expected ++ "-byte signature (got " ++ show actual ++ ")"--data VerifiableSignatureV where- VerifiableSignatureV4 :: SignaturePayloadV 'SigPayloadV4 -> VerifiableSignatureV- VerifiableSignatureV6 :: SignaturePayloadV 'SigPayloadV6 -> VerifiableSignatureV--fromSignaturePayloadVerifiableSignatureV ::- SignaturePayload -> Maybe VerifiableSignatureV-fromSignaturePayloadVerifiableSignatureV sigPayload =- case toSomeSignaturePayload sigPayload of- SomeSignaturePayload (payload@SigPayloadV4Data {}) ->- Just (VerifiableSignatureV4 payload)- SomeSignaturePayload (payload@SigPayloadV6Data {}) ->- Just (VerifiableSignatureV6 payload)- _ -> Nothing--isVerifiableSignaturePayload :: SignaturePayload -> Bool-isVerifiableSignaturePayload = isJust . fromSignaturePayloadVerifiableSignatureV--toSignaturePayloadFromVerifiable :: VerifiableSignatureV -> SignaturePayload-toSignaturePayloadFromVerifiable (VerifiableSignatureV4 payload) =- toSignaturePayload payload-toSignaturePayloadFromVerifiable (VerifiableSignatureV6 payload) =- toSignaturePayload payload--fromPktEitherVerifiableSignatureV :: Pkt -> Either VerificationError VerifiableSignatureV-fromPktEitherVerifiableSignatureV (SignaturePkt sigPayload) =- case fromSignaturePayloadVerifiableSignatureV sigPayload of- Just verifiableSig -> Right verifiableSig- Nothing ->- verificationError UnexpectedSignaturePayloadShape-fromPktEitherVerifiableSignatureV _ =- verificationError NonSignaturePacket--signaturePKAAndMPIsFromClass ::- SomeSignatureV- -> Either VerificationError (PubKeyAlgorithm, NonEmpty MPI)-signaturePKAAndMPIsFromClass =- fmap (\(pka, _, mpis) -> (pka, mpis)) . signatureVerificationMaterialFromClass--signatureLeft16FromClass :: SomeSignatureV -> Either VerificationError Word16-signatureLeft16FromClass =- fmap (\(_, l16, _) -> l16) . signatureVerificationMaterialFromClass--signatureVerificationMaterialFromClass ::- SomeSignatureV- -> Either VerificationError (PubKeyAlgorithm, Word16, NonEmpty MPI)-signatureVerificationMaterialFromClass (SomeSignatureV typedSig) =- case typedSig of- SignatureV3Packet (SigPayloadV3Data _ _ _ pka _ l16 mpis) ->- Right (pka, l16, mpis)- SignatureV4Packet (SigPayloadV4Data _ pka _ _ _ l16 mpis) ->- Right (pka, l16, mpis)- SignatureV6Packet (SigPayloadV6Data _ pka _ _ _ _ l16 mpis) ->- Right (pka, l16, mpis)- _ ->- verificationError UnexpectedSignaturePayloadShape--verifySigWith ::- (Pkt -> Maybe UTCTime -> ByteString -> Either VerificationError Verification)- -> Pkt- -> PktStreamContext- -> Maybe UTCTime- -> Either VerificationError Verification-verifySigWith vf sig@(SignaturePkt _) state mt =- case fromPktEitherVerifiableSignatureV sig of- Right verifiableSig ->- let (st, hs, us, checkSubpacket, checkUnhashedSubpackets) =- verifiableSignatureVerificationInputs verifiableSig- in checkUnhashedSubpackets us *>- verifyWithSubpacketChecks vf sig state mt st hs checkSubpacket- Left err ->- Left err- where- checkV4Subpacket signer i@Issuer {} = checkIssuerSubpacket (eightOctetKeyID signer) i- checkV4Subpacket signer i@IssuerFingerprint {} =- checkIssuerFingerprintSubpacket PKA.IssuerFingerprintV4 (fingerprint signer) i- checkV4Subpacket _ _ = Right True- -- RFC 9580 §5.2.3.35: v6 signatures MUST NOT include an Issuer Key ID subpacket.- -- Treat any such subpacket as a verification error rather than merely uncheckable.- checkV6Subpacket _ Issuer {} =- verificationError IssuerKeyIdProhibitedInV6Signature- checkV6Subpacket signer i@IssuerFingerprint {} =- checkIssuerFingerprintSubpacket PKA.IssuerFingerprintV6 (fingerprint signer) i- checkV6Subpacket _ _ = Right True- rejectV6UnhashedIssuer (SigSubPacket _ Issuer {}) =- verificationError IssuerKeyIdProhibitedInV6Signature- rejectV6UnhashedIssuer _ = Right ()- verifiableSignatureVerificationInputs ::- VerifiableSignatureV- ->- ( SigType- , [SigSubPacket]- , [SigSubPacket]- , SomePKPayload -> SigSubPacketPayload -> Either VerificationError Bool- , [SigSubPacket] -> Either VerificationError ()- )- verifiableSignatureVerificationInputs- (VerifiableSignatureV4 (SigPayloadV4Data st _ _ hs us _ _)) =- (st, hs, us, checkV4Subpacket, const (Right ()))- verifiableSignatureVerificationInputs- (VerifiableSignatureV6 (SigPayloadV6Data st _ _ _ hs us _ _)) =- (st, hs, us, checkV6Subpacket, mapM_ rejectV6UnhashedIssuer)-verifySigWith _ _ _ _ =- verificationError NonSignaturePacket--verifyWithSubpacketChecks ::- (Pkt -> Maybe UTCTime -> ByteString -> Either VerificationError Verification)- -> Pkt- -> PktStreamContext- -> Maybe UTCTime- -> SigType- -> [SigSubPacket]- -> (SomePKPayload -> SigSubPacketPayload -> Either VerificationError Bool)- -> Either VerificationError Verification-verifyWithSubpacketChecks vf sig state mt sigType hashedSubpackets checkSubpacket = do- mapM_ (rejectUnsupportedCriticalSubpacket sigType) hashedSubpackets- v <- vf sig mt (payloadForSig sigType state)- mapM_ (checkSubpacket (v ^. verificationSigner) . _sspPayload) hashedSubpackets- warnings <-- if sigType == SubkeyBindingSig- then verifySubkeyBackSignatures state mt hashedSubpackets- else Right []- isSignatureExpired sig mt *>- pure- (v- { _verificationWarnings =- _verificationWarnings v ++ warnings- })---- | Verify embedded primary-key back-signatures (PrimaryKeyBindingSig, 0x19)--- found in the hashed subpackets of a SubkeyBindingSig.------ Per RFC 9580 §5.2.3.3, a signing-capable subkey MUST include an embedded--- PrimaryKeyBindingSig (0x19) made by the subkey. This requirement is--- enforced strictly for v6 subkeys. For v4 subkeys we still verify any--- embedded back-sigs that are present, but do not reject a missing one —--- real-world v4 signing subkeys predate the strict cross-certification--- mandate and widespread interoperability requires accepting them.-verifySubkeyBackSignatures ::- PktStreamContext- -> Maybe UTCTime- -> [SigSubPacket]- -> Either VerificationError [VerificationWarning]-verifySubkeyBackSignatures state mt hashedSubpackets = do- subkeyPKP <-- maybe- (verificationError NonSignaturePacket)- Right- (subkeyPKPFromPkt (lastSubkey state))- let embeddedSigs =- [ sp- | SigSubPacket _ (EmbeddedSignature sp) <- hashedSubpackets- ]- isSigningCapable =- any- (\(SigSubPacket _ payload) ->- case payload of- KeyFlags flags -> SignDataKey `Set.member` flags- _ -> False)- hashedSubpackets- isV6Subkey = _keyVersion subkeyPKP == V6- case embeddedSigs of- [] ->- -- Require back-sig only for v6 signing subkeys (RFC 9580 §5.2.3.3).- if isSigningCapable && isV6Subkey- then Right [MissingSubkeyBackSignatureWarning]- else Right []- _ ->- -- Always verify back-sigs that are present, regardless of key version.- mapM_ (verifyOneBackSig subkeyPKP) embeddedSigs *> Right []- where- verifyOneBackSig subkeyPKP embSigPayload = do- let embSigPkt = SignaturePkt embSigPayload- backSigContext =- emptyPSC- { lastPrimaryKey = lastPrimaryKey state- , lastSubkey = lastSubkey state- }- case verifyAgainstKey' subkeyPKP embSigPkt mt- (payloadForSig PrimaryKeyBindingSig backSigContext) of- Left err -> verificationError (InvalidSubkeyBackSignature err)- Right _ -> Right ()--rejectUnsupportedCriticalSubpacket :: SigType -> SigSubPacket -> Either VerificationError ()-rejectUnsupportedCriticalSubpacket sigType (SigSubPacket isCritical payload)- | not isCritical = Right ()- | not (isBindingSignatureType sigType) = Right ()- | otherwise =- case payload of- UserDefinedSigSub {} -> verificationError (UnsupportedCriticalSubpacket sigType)- OtherSigSub {} -> verificationError (UnsupportedCriticalSubpacket sigType)- _ -> Right ()--isBindingSignatureType :: SigType -> Bool-isBindingSignatureType SubkeyBindingSig = True-isBindingSignatureType PrimaryKeyBindingSig = True-isBindingSignatureType _ = False--checkIssuerSubpacket ::- Either String EightOctetKeyId- -> SigSubPacketPayload- -> Either VerificationError Bool-checkIssuerSubpacket (Right signer) (Issuer i)- | signer == i = Right True- | otherwise = verificationError IssuerSubpacketMismatch-checkIssuerSubpacket (Left err) (Issuer _) =- verificationError (IssuerSubpacketUncheckable err)-checkIssuerSubpacket _ _ = Right True--checkIssuerFingerprintSubpacket ::- IssuerFingerprintVersion -> Fingerprint -> SigSubPacketPayload -> Either VerificationError Bool-checkIssuerFingerprintSubpacket expectedVersion signer (IssuerFingerprint kv i)- | kv /= expectedVersion = verificationError IssuerFingerprintSubpacketMismatch- | signer == i = Right True- | otherwise = verificationError IssuerFingerprintSubpacketMismatch-checkIssuerFingerprintSubpacket _ _ _ = Right True--verifyTKWith ::- (Pkt -> PktStreamContext -> Maybe UTCTime -> Either VerificationError Verification)- -> Maybe UTCTime- -> TK k- -> Either VerificationError (TK k)-verifyTKWith vsf mt tk = do- verifiedUnknown <- verifyUnknownTKWith vsf mt (tkToUnknown tk)- let typedSubkeys = Map.fromList [ (keyPktToPkt kp, kp) | (kp, _) <- tk ^. tkSubs ]- verifiedTypedSubkeys =- mapMaybe- (\(pkt, sigs) -> (\kp -> (kp, sigs)) <$> Map.lookup pkt typedSubkeys)- (verifiedUnknown ^. tkuSubs)- pure- TK- { _tkPrimaryKey = tk ^. tkPrimaryKey- , _tkRevs = verifiedUnknown ^. tkuRevs- , _tkUIDs = verifiedUnknown ^. tkuUIDs- , _tkUAts = verifiedUnknown ^. tkuUAts- , _tkSubs = verifiedTypedSubkeys- }--verifyUnknownTKWith ::- (Pkt -> PktStreamContext -> Maybe UTCTime -> Either VerificationError Verification)- -> Maybe UTCTime- -> TKUnknown- -> Either VerificationError TKUnknown-verifyUnknownTKWith vsf mt tk = do- revokers <- checkRevokers tk- revs <- checkKeyRevocations revokers tk- let uids = filter (not . null . snd) . checkUidSigs $ tk ^. tkuUIDs- let uats = filter (not . null . snd) . checkUAtSigs $ tk ^. tkuUAts- let subs = concatMap checkSub $ tk ^. tkuSubs- return (TKUnknown (tk ^. tkuKey) revs uids uats subs)- where- checkRevokers =- Right . concat . rights . map verifyRevoker . filter isRevokerP . _tkuRevs- checkKeyRevocations ::- [(PubKeyAlgorithm, Fingerprint)]- -> TKUnknown- -> Either VerificationError [SignaturePayload]- checkKeyRevocations rs k =- Prelude.sequence . concatMap (filterRevs rs) . rights .- map (liftM2 fmap (,) vSig) $- k ^.- tkuRevs- checkUidSigs :: [(Text, [SignaturePayload])] -> [(Text, [SignaturePayload])]- checkUidSigs =- map- (\(uid, sps) ->- let verified = rights . map (\sp -> fmap ((,) sp) (vUid (uid, sp))) $ sps- in (uid, retainNonRevokedCertifications mt verified))- checkUAtSigs ::- [([UserAttrSubPacket], [SignaturePayload])]- -> [([UserAttrSubPacket], [SignaturePayload])]- checkUAtSigs =- map- (\(uat, sps) ->- let verified = rights . map (\sp -> fmap ((,) sp) (vUAt (uat, sp))) $ sps- in (uat, retainNonRevokedCertifications mt verified))- checkSub :: (Pkt, [SignaturePayload]) -> [(Pkt, [SignaturePayload])]- checkSub (pkt, sps) =- if revokedSub pkt sps- then []- else checkSub' pkt sps- revokedSub :: Pkt -> [SignaturePayload] -> Bool- revokedSub _ [] = False- revokedSub p sigs =- any (vSubSig p) (filter subkeyRevocationEffective sigs)- checkSub' :: Pkt -> [SignaturePayload] -> [(Pkt, [SignaturePayload])]- checkSub' p sps =- let goodsigs =- filter (vSubSig p) .- filter signatureKnown .- filter isSubkeyBindingSig $- sps- in if null goodsigs- then []- else [(p, goodsigs)]- getHasheds = signatureHashedSubpackets- filterRevs ::- [(PubKeyAlgorithm, Fingerprint)]- -> (SignaturePayload, Verification)- -> [Either VerificationError SignaturePayload]- filterRevs vokers spv =- case spv of- (s, _)- | isV4OrV6Sig s && sigType s == Just SignatureDirectlyOnAKey ->- [Right s | signatureKnown s]- (s, v)- | isV4OrV6Sig s- , sigType s == Just KeyRevocationSig- , Just pka <- sigPKA s ->- if (v ^. verificationSigner == tk ^. tkuKey . _1) ||- any- (\(p, f) ->- p == pka && f == fingerprint (v ^. verificationSigner))- vokers- then- if keyRevocationEffective s- then [verificationError KeyRevoked]- else [Right s | signatureKnown s]- else [Right s | signatureKnown s]- _ -> []- isV4OrV6Sig = isVerifiableSignaturePayload- vUid :: (Text, SignaturePayload) -> Either VerificationError Verification- vUid (uid, sp) =- vsf- (SignaturePkt sp)- emptyPSC- { lastPrimaryKey = PublicKeyPkt (tk ^. tkuKey . _1)- , lastUIDorUAt = UserIdPkt uid- }- Nothing- vUAt ::- ([UserAttrSubPacket], SignaturePayload) -> Either VerificationError Verification- vUAt (uat, sp) =- vsf- (SignaturePkt sp)- emptyPSC- { lastPrimaryKey = PublicKeyPkt (tk ^. tkuKey . _1)- , lastUIDorUAt = UserAttributePkt uat- }- Nothing- vSig :: SignaturePayload -> Either VerificationError Verification- vSig sp =- vsf- (SignaturePkt sp)- emptyPSC {lastPrimaryKey = PublicKeyPkt (tk ^. tkuKey . _1)}- Nothing- vSubSig :: Pkt -> SignaturePayload -> Bool- vSubSig sk sp =- isRight- (vsf- (SignaturePkt sp)- emptyPSC- { lastPrimaryKey = PublicKeyPkt (tk ^. tkuKey . _1)- , lastSubkey = sk- }- mt)- verifyRevoker ::- SignaturePayload- -> Either VerificationError [(PubKeyAlgorithm, Fingerprint)]- verifyRevoker sp =- vSig sp *>- pure- (map (\(SigSubPacket _ (RevocationKey _ pka fp)) -> (pka, fp)) .- filter isRevocationKeySSP $- getHasheds sp)- retainNonRevokedCertifications ::- Maybe UTCTime -> [(SignaturePayload, Verification)] -> [SignaturePayload]- retainNonRevokedCertifications validationTime verified =- map fst $- filter- ((== CertificationActive) . certificationStateAt validationTime verified)- certifications- where- certifications = filter (not . isCertRevocationSig . fst) verified- signatureKnown = signatureKnownAt mt- subkeyRevocationEffective sp = isSubkeyRevocation sp && signatureEffectiveAt mt sp- keyRevocationEffective sp- | isHistoricalKeyRevocation sp = signatureEffectiveAt mt sp- | otherwise = signatureUnexpiredAt mt sp--verifyAgainstKeyring ::- PublicKeyring -> Pkt -> Maybe UTCTime -> ByteString -> Either VerificationError Verification-verifyAgainstKeyring kr sig mt payload = do- let allKeys = map tkToUnknown (IxSet.toList kr)- signerValidationTime = signatureCreationTimeFromPacket sig- ikeys = (kr @=) <$> issuer sig- ifpkeys = (kr @=) <$> issuerFP sig- hintedKeys = maybe [] (map tkToUnknown . IxSet.toList) (ifpkeys <|> ikeys)- hintedResult =- if null hintedKeys- then Left MissingIssuer- else verifyFromCandidates allKeys hintedKeys sig signerValidationTime mt payload- in case hintedResult of- Right v -> Right v- Left hintedErr ->- let fallbackResult =- verifyFromCandidates allKeys allKeys sig signerValidationTime mt payload- in if null hintedKeys- then- case fallbackResult of- Right v -> Right v- Left _ ->- verificationError (SigningKeyNotFound (issuer sig) (issuerFP sig))- else- case fallbackResult of- Right v -> Right v- Left _ -> Left hintedErr--verifyFromCandidates ::- [TKUnknown]- -> [TKUnknown]- -> Pkt- -> Maybe UTCTime- -> Maybe UTCTime- -> ByteString- -> Either VerificationError Verification-verifyFromCandidates allKeys candidateTks sig signerValidationTime verificationTime payload =- let candidateResults =- map- (resolveCandidateSignerPKPs allKeys sig signerValidationTime (const True))- candidateTks- candidateErrors = concatMap fst candidateResults- usablePkps = concatMap snd candidateResults- in if null usablePkps- then- if null candidateErrors- then verificationError (SigningKeyNotFound (issuer sig) (issuerFP sig))- else verificationError (CandidateKeyFailures candidateErrors)- else- case verifyAgainstPKPs usablePkps sig verificationTime payload of- Left (CandidateKeyFailures errs)- | not (null candidateErrors) ->- verificationError (CandidateKeyFailures (candidateErrors ++ errs))- other -> other--verifyAgainstKeys ::- [TKUnknown] -> Pkt -> Maybe UTCTime -> ByteString -> Either VerificationError Verification-verifyAgainstKeys ks sig mt payload = do- let allpkps =- filter- (\x ->- (((fingerprint x ==) <$> issuerFP sig) == Just True) ||- ((==) <$> issuer sig <*> hush (eightOctetKeyID x)) ==- Just True)- (concatMap (\x -> (x ^. tkuKey . _1) : mapMaybe (subkeyPKPFromPkt . fst) (_tkuSubs x)) ks)- allCandidatePkps =- concatMap (\x -> (x ^. tkuKey . _1) : mapMaybe (subkeyPKPFromPkt . fst) (_tkuSubs x)) ks- normalizedCandidates- | null allpkps = allCandidatePkps- | otherwise = allpkps- verifyAgainstPKPs normalizedCandidates sig mt payload--verifyAgainstPKPs ::- [SomePKPayload] -> Pkt -> Maybe UTCTime -> ByteString -> Either VerificationError Verification-verifyAgainstPKPs pkps sig mt payload =- case rights results of- [] -> verificationError (CandidateKeyFailures (lefts results))- [r] -> isSignatureExpired sig mt *> pure r- rs -> verificationError (MultipleVerificationSuccesses (length rs))- where- results = map (\pkp -> verifyAgainstKey' pkp sig mt payload) pkps--resolveCandidateSignerPKPs ::- [TKUnknown]- -> Pkt- -> Maybe UTCTime- -> (SomePKPayload -> Bool)- -> TKUnknown- -> ([VerificationError], [SomePKPayload])-resolveCandidateSignerPKPs _ _ Nothing matchesP tk =- ([], filter matchesP (candidatePKPs tk))-resolveCandidateSignerPKPs allKeys _ (Just validationTime) matchesP tk =- let rawMatches = filter matchesP (candidatePKPs tk)- in case verifyUnknownTKWith (verifySigWith (verifyAgainstKeys allKeys)) (Just validationTime) tk of- Left err -> (replicate (length rawMatches) err, [])- Right verifiedTK ->- let verifiedMatches =- map- (\pkp ->- case historicallyValidSigner validationTime (timelineValidationTK pkp verifiedTK) pkp of- Right () -> Right pkp- Left err -> Left err)- rawMatches- in (lefts verifiedMatches, rights verifiedMatches)- where- timelineValidationTK pkp verifiedTK'- | fingerprint pkp == fingerprint (tk ^. tkuKey . _1) = tk- | otherwise = verifiedTK'--candidatePKPs :: TKUnknown -> [SomePKPayload]-candidatePKPs tk = (tk ^. tkuKey . _1) : mapMaybe (subkeyPKPFromPkt . fst) (tk ^. tkuSubs)--historicallyValidSigner :: - UTCTime -> TKUnknown -> SomePKPayload -> Either VerificationError ()-historicallyValidSigner validationTime tk pkp- | fingerprint pkp == fingerprint (tk ^. tkuKey . _1) =- if keyStateValid (keyStateAt validationTime tk)- then Right ()- else verificationError SigningKeyUnavailableAtSignatureTime- | otherwise =- case find (\(pkt, _) -> maybe False ((== fingerprint pkp) . fingerprint) (subkeyPKPFromPkt pkt)) (tk ^. tkuSubs) of- Nothing -> verificationError SigningKeyUnavailableAtSignatureTime- Just (subPkt, sigs) ->- case subkeyPKPFromPkt subPkt of- Nothing -> verificationError SigningKeyUnavailableAtSignatureTime- Just subPKP ->- if isPKTimeValidWithSelfSignatures validationTime subPKP sigs- then Right ()- else verificationError SigningKeyUnavailableAtSignatureTime--signatureCreationTimeFromPacket :: Pkt -> Maybe UTCTime-signatureCreationTimeFromPacket (SignaturePkt sigPayload) = signatureCreationTime sigPayload-signatureCreationTimeFromPacket _ = Nothing--signatureCreationTime :: SignaturePayload -> Maybe UTCTime-signatureCreationTime =- fmap (posixSecondsToUTCTime . realToFrac . unThirtyTwoBitTimeStamp) . sigCT--signatureKnownAt :: Maybe UTCTime -> SignaturePayload -> Bool-signatureKnownAt Nothing _ = True-signatureKnownAt (Just validationTime) sigPayload =- maybe False (<= validationTime) (signatureCreationTime sigPayload)--signatureEffectiveAt :: Maybe UTCTime -> SignaturePayload -> Bool-signatureEffectiveAt Nothing _ = True-signatureEffectiveAt (Just validationTime) sigPayload =- signatureKnownAt (Just validationTime) sigPayload &&- maybe True (validationTime <) (signatureExpirationTime sigPayload)--signatureUnexpiredAt :: Maybe UTCTime -> SignaturePayload -> Bool-signatureUnexpiredAt Nothing _ = True-signatureUnexpiredAt (Just validationTime) sigPayload =- maybe True (validationTime <) (signatureExpirationTime sigPayload)--signatureExpirationTime :: SignaturePayload -> Maybe UTCTime-signatureExpirationTime sigPayload =- addDurationToTime <$>- signatureCreationTime sigPayload <*>- signatureExpirationDuration sigPayload--signatureExpirationDuration :: SignaturePayload -> Maybe ThirtyTwoBitDuration-signatureExpirationDuration sigPayload =- signatureHashedSubpacketsKnown sigPayload >>= firstSignatureExpirationDuration--firstSignatureExpirationDuration :: [SigSubPacket] -> Maybe ThirtyTwoBitDuration-firstSignatureExpirationDuration =- foldr- (\subpacket acc ->- case subpacket of- SigSubPacket _ (SigExpirationTime duration) -> Just duration- _ -> acc)- Nothing--addDurationToTime :: UTCTime -> ThirtyTwoBitDuration -> UTCTime-addDurationToTime baseTime duration =- addUTCTime (fromIntegral (unThirtyTwoBitDuration duration)) baseTime--certificationStateAt ::- Maybe UTCTime- -> [(SignaturePayload, Verification)]- -> (SignaturePayload, Verification)- -> CertificationState-certificationStateAt validationTime verified certification@(certificationSig, _)- | not (signatureKnownAt validationTime certificationSig) = CertificationNotYetKnown- | not (signatureEffectiveAt validationTime certificationSig) = CertificationRevoked- | any (`revokesCertification` certification) visibleRevocations = CertificationRevoked- | otherwise = CertificationActive- where- visibleRevocations =- filter- (signatureEffectiveAt validationTime . fst)- (filter (isCertRevocationSig . fst) verified)--revokesCertification ::- (SignaturePayload, Verification)- -> (SignaturePayload, Verification)- -> Bool-revokesCertification (revocationSig, revocationVerification) (certificationSig, certificationVerification) =- sameSigner && certificationPrecedesRevocation certificationSig revocationSig- where- sameSigner =- fingerprint (revocationVerification ^. verificationSigner) ==- fingerprint (certificationVerification ^. verificationSigner)--certificationPrecedesRevocation ::- SignaturePayload -> SignaturePayload -> Bool-certificationPrecedesRevocation certificationSig revocationSig =- case (signatureCreationTime certificationSig, signatureCreationTime revocationSig) of- (Just certificationTime, Just revocationTime) ->- certificationTime < revocationTime- _ -> False--isHistoricalKeyRevocation :: SignaturePayload -> Bool-isHistoricalKeyRevocation sigPayload =- case revocationReasonCode sigPayload of- Just KeySuperseded -> True- Just KeyRetiredAndNoLongerUsed -> True- Just UserIdInfoNoLongerValid -> True- _ -> False--revocationReasonCode :: SignaturePayload -> Maybe RevocationCode-revocationReasonCode sigPayload =- (\(SigSubPacket _ (ReasonForRevocation reasonCode _)) -> reasonCode) <$>- find isReasonForRevocation (signatureSubpackets sigPayload)- where- isReasonForRevocation (SigSubPacket _ ReasonForRevocation {}) = True- isReasonForRevocation _ = False--signatureSubpackets :: SignaturePayload -> [SigSubPacket]-signatureSubpackets sigPayload =- case signatureSubpacketListsKnown sigPayload of- Just (hashed, unhashed) -> hashed ++ unhashed- Nothing -> []--signatureHashedSubpackets :: SignaturePayload -> [SigSubPacket]-signatureHashedSubpackets sigPayload =- maybe [] id (signatureHashedSubpacketsKnown sigPayload)--subkeyPKPFromPkt :: Pkt -> Maybe SomePKPayload-subkeyPKPFromPkt (PublicSubkeyPkt p) = Just p-subkeyPKPFromPkt (SecretSubkeyPkt p _) = Just p-subkeyPKPFromPkt _ = Nothing--verifyAgainstKey' :: SomePKPayload -> Pkt -> Maybe UTCTime -> ByteString -> Either VerificationError Verification-verifyAgainstKey' pkp sig mt payload = do- sigClass <-- either- (verificationError . const NonSignaturePacket)- Right- (fromPktEitherSomeSignatureV sig)- let sigPayload =- case sigClass of- SomeSignatureV typedSig -> signaturePayloadFromSignatureV typedSig- sigHash <-- maybe- (verificationError MissingHashAlgorithm)- Right- (sigHA sigPayload)- sigDetails <- signaturePKAAndMPIsFromClass sigClass- enforcePKACompatibility sigPayload- enforceSignatureHashPolicy sigHash- _ <- isSignatureExpired sig mt- let signedPayload = BL.toStrict (finalPayload sig payload)- enforceLeft16Prefix sigClass sigHash signedPayload- (\verifiedSigner -> Verification verifiedSigner sigPayload []) <$>- verify' sigDetails pkp sigHash signedPayload- where- enforcePKACompatibility sigPayload =- let sigPka = maybe (OtherPKA 0) id (sigPKA sigPayload)- keyPka = _pkalgo pkp- in if pkaCompatible sigPka keyPka- then Right ()- else verificationError (SignaturePolicyPKAMismatch sigPka keyPka)- enforceSignatureHashPolicy sigHash =- case sigHash of- OtherHA {} -> verificationError (SignaturePolicyHashUnsupported sigHash)- _ -> Right ()- enforceLeft16Prefix sigClass sigHash signedPayload = do- expectedLeft16 <-- either- (verificationError . HashComputationFailed)- Right- (left16FromSignedPayload sigHash signedPayload)- actualLeft16 <- signatureLeft16FromClass sigClass- if actualLeft16 == expectedLeft16- then Right ()- else- verificationError- (SignatureMismatch (_pkalgo pkp) (fingerprint pkp))- pkaCompatible RSA keyPka =- keyPka `elem` [RSA, DeprecatedRSAEncryptOnly, DeprecatedRSASignOnly]- pkaCompatible DeprecatedRSASignOnly keyPka =- keyPka `elem` [RSA, DeprecatedRSASignOnly]- pkaCompatible PKA.EdDSA keyPka =- keyPka `elem` [PKA.EdDSA, PKA.Ed25519, PKA.Ed448]- pkaCompatible PKA.Ed25519 keyPka =- keyPka `elem` [PKA.EdDSA, PKA.Ed25519]- pkaCompatible PKA.Ed448 keyPka =- keyPka `elem` [PKA.EdDSA, PKA.Ed448]- pkaCompatible sigPka keyPka = sigPka == keyPka- verify' details pub@(PKPayload V4 _ _ _ pkey) ha pl =- verifyByHash details pub pkey ha pl- verify' details pub@(PKPayload V6 _ _ _ pkey) ha pl =- verifyByHash details pub pkey ha pl- verify' _ _ _ _ =- verificationError UnexpectedKeyVersion- verifyByHash details pub pkey ha pl =- case ha of- SHA1 -> verify'' details CHA.SHA1 pub pkey pl- RIPEMD160 -> verify'' details CHA.RIPEMD160 pub pkey pl- SHA224 -> verify'' details CHA.SHA224 pub pkey pl- SHA256 -> verify'' details CHA.SHA256 pub pkey pl- SHA384 -> verify'' details CHA.SHA384 pub pkey pl- SHA512 -> verify'' details CHA.SHA512 pub pkey pl- SHA3_256 -> verifyNoRSA SHA3_256 details CHA.SHA3_256 pub pkey pl- SHA3_512 -> verifyNoRSA SHA3_512 details CHA.SHA3_512 pub pkey pl- DeprecatedMD5 -> verify'' details CHA.MD5 pub pkey pl- _ ->- verificationError (SignaturePolicyHashUnsupported ha)- verify'' (DSA, mpis) hd pub (DSAPubKey (DSA_PublicKey pkey)) bs =- dsaVerify pub mpis hd pkey bs- verify'' (ECDSA, mpis) hd pub (ECDSAPubKey (ECDSA_PublicKey pkey)) bs =- ecdsaVerify pub mpis hd pkey bs- verify'' (sigPka, mpis) hd pub (EdDSAPubKey Ed25519 pkey) bs- | sigPka `elem` [EdDSA, PKA.Ed25519] =- ed25519Verify sigPka pub mpis hd pkey bs- verify'' (sigPka, mpis) hd pub (EdDSAPubKey Ed448 pkey) bs- | sigPka `elem` [EdDSA, PKA.Ed448] =- ed448Verify sigPka pub mpis hd pkey bs- verify'' (RSA, mpis) hd pub (RSAPubKey (RSA_PublicKey pkey)) bs =- rsaVerify pub mpis hd pkey bs- verify'' (pka, _) _ _ _ _ = verificationError (UnsupportedKeyType pka)- verifyNoRSA ha' (DSA, mpis) hd pub (DSAPubKey (DSA_PublicKey pkey)) bs =- dsaVerify pub mpis hd pkey bs- verifyNoRSA ha' (ECDSA, mpis) hd pub (ECDSAPubKey (ECDSA_PublicKey pkey)) bs =- ecdsaVerify pub mpis hd pkey bs- verifyNoRSA ha' (sigPka, mpis) hd pub (EdDSAPubKey Ed25519 pkey) bs- | sigPka `elem` [EdDSA, PKA.Ed25519] =- ed25519Verify sigPka pub mpis hd pkey bs- verifyNoRSA ha' (sigPka, mpis) hd pub (EdDSAPubKey Ed448 pkey) bs- | sigPka `elem` [EdDSA, PKA.Ed448] =- ed448Verify sigPka pub mpis hd pkey bs- verifyNoRSA ha' (RSA, _) _ _ _ _ =- verificationError (SignatureHashUnsupportedByAlgorithm ha' RSA)- verifyNoRSA _ (pka, _) _ _ _ _ = verificationError (UnsupportedKeyType pka)- dsaVerify pub (r :| [s]) hd pkey bs =- if DSA.verify hd pkey (dsaMPIsToSig r s) bs- then Right pub- else verificationError (SignatureMismatch DSA (fingerprint pub))- dsaVerify _ _ _ _ _ = verificationError (SignatureShapeMismatch DSA)- ecdsaVerify pub (r :| [s]) hd pkey bs =- if ECDSA.verify hd pkey (ecdsaMPIsToSig r s) bs- then Right pub- else verificationError (SignatureMismatch ECDSA (fingerprint pub))- ecdsaVerify _ _ _ _ _ = verificationError (SignatureShapeMismatch ECDSA)- ed25519Verify sigPka pub (r :| [s]) hd pkey bs =- case edPointToRawPublic 32 pkey of- Left err ->- verificationError (SignatureEncodingInvalid sigPka err)- Right rawPub ->- case cf2es (Ed25519.publicKey rawPub) of- Left err ->- verificationError (SignatureEncodingInvalid sigPka err)- Right ep ->- case cf2es (Ed25519.signature (pad32 (i2osp (unMPI r)) <> pad32 (i2osp (unMPI s)))) of- Left err ->- verificationError (SignatureEncodingInvalid sigPka err)- Right es ->- let prehash = crazyHash hd bs :: B.ByteString- in if Ed25519.verify ep prehash es- then Right pub- else verificationError (SignatureMismatch sigPka (fingerprint pub))- ed25519Verify sigPka _ _ _ _ _ =- verificationError (SignatureShapeMismatch sigPka)- ed448Verify sigPka pub (r :| [s]) hd pkey bs =- case edPointToRawPublic 57 pkey of- Left err ->- verificationError (SignatureEncodingInvalid sigPka err)- Right rawPub ->- case cf2es (Ed448.publicKey rawPub) of- Left err ->- verificationError (SignatureEncodingInvalid sigPka err)- Right ep ->- case cf2es (Ed448.signature (padN 57 (i2osp (unMPI r)) <> padN 57 (i2osp (unMPI s)))) of- Left err ->- verificationError (SignatureEncodingInvalid sigPka err)- Right es ->- let prehash = crazyHash hd bs :: B.ByteString- in if Ed448.verify ep prehash es- then Right pub- else verificationError (SignatureMismatch sigPka (fingerprint pub))- ed448Verify sigPka _ _ _ _ _ =- verificationError (SignatureShapeMismatch sigPka)- edPointToRawPublic expectedLen (NativeEPoint (EPoint x)) =- exactLengthPublic expectedLen "native" (i2osp x)- edPointToRawPublic expectedLen (PrefixedNativeEPoint (EPoint x)) = do- prefixed <- exactLengthPublic (expectedLen + 1) "prefixed-native" (i2osp x)- if B.head prefixed /= 0x40- then Left "prefixed-native EdDSA public key is missing the 0x40 prefix"- else Right (B.tail prefixed)- exactLengthPublic expectedLen label bs- | B.length bs == expectedLen = Right bs- | otherwise =- Left- ("invalid " ++ label ++ " EdDSA public key length: expected " ++- show expectedLen ++ " octets, got " ++ show (B.length bs))- pad32 bs =- let l = B.length bs- in if l >= 32- then bs- else B.replicate (32 - l) 0 <> bs- padN n bs =- let l = B.length bs- in if l >= n- then bs- else B.replicate (n - l) 0 <> bs- cf2es = either (Left . show) return . 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))- 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)- pad = sz - B.length raw- in B.replicate pad 0 <> raw- crazyHash h = BA.convert . hashWith h--isSignatureExpired :: Pkt -> Maybe UTCTime -> Either VerificationError Bool-isSignatureExpired _ Nothing = return False-isSignatureExpired s (Just t) =- do- sigClass <-- either- (verificationError . const NonSignaturePacket)- Right- (fromPktEitherSomeSignatureV s)- if any- (expiredBefore t)- (signatureHashedSubpackets- (case sigClass of- SomeSignatureV typedSig -> signaturePayloadFromSignatureV typedSig))- then verificationError SignatureExpired- else return True- where- expiredBefore :: UTCTime -> SigSubPacket -> Bool- expiredBefore ct (SigSubPacket _ (SigExpirationTime et)) =- fromEnum ((posixSecondsToUTCTime . toEnum . fromEnum) et `diffUTCTime` ct) <- 0- expiredBefore _ _ = False--finalPayload :: Pkt -> ByteString -> ByteString-finalPayload s pl = BL.concat [pl, sigbit, trailer s]- where- sigbit = runPut $ putPartialSigforSigning s- trailer :: Pkt -> ByteString- trailer (SignaturePkt sigPayload) =- maybe- BL.empty- (const (runPut $ putSigTrailer s))- (fromSignaturePayloadVerifiableSignatureV sigPayload)- trailer _ = BL.empty--normalizePayloadForSigType :: SigType -> ByteString -> ByteString-normalizePayloadForSigType CanonicalTextSig =- stripTrailingWhitespacePerLine . canonicalizeLineEndings-normalizePayloadForSigType _ = id--normalizePayloadForSigTypeWith :: TextNormalizationMode -> SigType -> ByteString -> ByteString-normalizePayloadForSigTypeWith CleartextCompat st = normalizePayloadForSigType st-normalizePayloadForSigTypeWith RFC9580Strict CanonicalTextSig = canonicalizeLineEndings-normalizePayloadForSigTypeWith RFC9580Strict _ = id--canonicalizeLineEndings :: ByteString -> ByteString-canonicalizeLineEndings = BL.pack . go . BL.unpack- where- go [] = []- go (0x0d:0x0a:rest) = 0x0d : 0x0a : go rest- go (0x0d:rest) = 0x0d : 0x0a : go rest- go (0x0a:rest) = 0x0d : 0x0a : go rest- go (w:rest) = w : go rest--stripTrailingWhitespacePerLine :: ByteString -> ByteString-stripTrailingWhitespacePerLine = BL.pack . go [] . BL.unpack- where- go lineRev [] = reverseTrimmed lineRev- go lineRev (0x0d:0x0a:rest) =- reverseTrimmed lineRev ++ [0x0d, 0x0a] ++ go [] rest- go lineRev (w:rest) = go (w : lineRev) rest-- reverseTrimmed :: [Word8] -> [Word8]- reverseTrimmed = reverse . dropWhile isTrailingWhitespace-- isTrailingWhitespace :: Word8 -> Bool- isTrailingWhitespace w = w == 0x20 || w == 0x09--hashWithSHA512 :: B.ByteString -> B.ByteString-hashWithSHA512 = BA.convert . hashWith CHA.SHA512--hashForSignatureAlgorithm :: HashAlgorithm -> B.ByteString -> Either String B.ByteString-hashForSignatureAlgorithm ha bs =- case ha of- SHA1 -> Right (BA.convert (hashWith CHA.SHA1 bs))- RIPEMD160 -> Right (BA.convert (hashWith CHA.RIPEMD160 bs))- SHA224 -> Right (BA.convert (hashWith CHA.SHA224 bs))- SHA256 -> Right (BA.convert (hashWith CHA.SHA256 bs))- SHA384 -> Right (BA.convert (hashWith CHA.SHA384 bs))- SHA512 -> Right (BA.convert (hashWith CHA.SHA512 bs))- SHA3_256 -> Right (BA.convert (hashWith CHA.SHA3_256 bs))- SHA3_512 -> Right (BA.convert (hashWith CHA.SHA3_512 bs))- DeprecatedMD5 -> Right (BA.convert (hashWith CHA.MD5 bs))- _ -> Left ("unsupported hash algorithm for left16 derivation: " ++ show ha)--left16FromHashPrefix :: B.ByteString -> Either String Word16-left16FromHashPrefix bs- | B.length bs >= 2 = Right (fromIntegral (os2ip (B.take 2 bs)))- | otherwise = Left "hash output too short to derive left16"--left16FromSignedPayload :: HashAlgorithm -> B.ByteString -> Either String Word16-left16FromSignedPayload ha signedPayload = do- digest <- hashForSignatureAlgorithm ha signedPayload- left16FromHashPrefix digest--left16FromSignedPayloadForSign :: HashAlgorithm -> B.ByteString -> Either SignError Word16-left16FromSignedPayloadForSign ha =- first SignBackendError . left16FromSignedPayload ha--ed25519Signer :: Ed25519.SecretKey -> B.ByteString -> B.ByteString-ed25519Signer sk prehash =- BA.convert (Ed25519.sign sk (Ed25519.toPublic sk) prehash)--ed448Signer :: Ed448.SecretKey -> B.ByteString -> B.ByteString-ed448Signer sk prehash =- BA.convert (Ed448.sign sk (Ed448.toPublic sk) prehash)--rsaPKCS15Sign ::- HashAlgorithm- -> RSATypes.PrivateKey- -> B.ByteString- -> Either SignError B.ByteString-rsaPKCS15Sign ha prv bytes =- case ha of- SHA1 -> first (SignBackendError . show) (P15.sign Nothing (Just CHA.SHA1) prv bytes)- SHA224 -> first (SignBackendError . show) (P15.sign Nothing (Just CHA.SHA224) prv bytes)- SHA256 -> first (SignBackendError . show) (P15.sign Nothing (Just CHA.SHA256) prv bytes)- SHA384 -> first (SignBackendError . show) (P15.sign Nothing (Just CHA.SHA384) prv bytes)- SHA512 -> first (SignBackendError . show) (P15.sign Nothing (Just CHA.SHA512) prv bytes)- _ ->- Left- (SignBackendError- ("signature hash algorithm is not supported by RSA PKCS#1 v1.5 backend: " ++- show ha))--validateV6SaltSize :: HashAlgorithm -> SignatureSalt -> Either SignError ()-validateV6SaltSize ha salt =- let saltBytes = BL.toStrict (unSignatureSalt salt)- actualSaltLen = B.length saltBytes- in case signatureV6SaltSizeForHashAlgorithm ha of- Nothing ->- Left- (SignBackendError- ("signature hash algorithm does not define a V6 salt size: " ++ show ha))- Just expectedSaltLen ->- if actualSaltLen == fromIntegral expectedSaltLen- then Right ()- else Left (SignV6SaltSizeMismatch ha expectedSaltLen actualSaltLen)--signEdDSAV4 ::- String- -> Int- -> Int- -> (B.ByteString -> B.ByteString)- -> TextNormalizationMode- -> SigType- -> PubKeyAlgorithm- -> HashAlgorithm- -> [SigSubPacket]- -> [SigSubPacket]- -> ByteString- -> Either SignError SignaturePayload-signEdDSAV4 algoName sigLen limbLen signer mode st pka ha has uhas payload = do- let normalizedPayload = normalizePayloadForSigTypeWith mode st payload- sig0 = SigV4 st pka ha has [] 0 (NE.fromList [MPI 0, MPI 0])- prehash =- hashWithSHA512- (BL.toStrict (finalPayload (SignaturePkt sig0) normalizedPayload))- sigBytes = signer prehash- if B.length sigBytes /= sigLen- then Left (SignProducedWrongLength algoName sigLen (B.length sigBytes))- else- let (r, s) = B.splitAt limbLen sigBytes- in (\left16 ->- SigV4- st- pka- ha- has- uhas- left16- (NE.fromList [MPI (os2ip r), MPI (os2ip s)]))- <$> first SignBackendError (left16FromHashPrefix prehash)--signEdDSAV6 ::- String- -> Int- -> Int- -> (B.ByteString -> B.ByteString)- -> TextNormalizationMode- -> SigType- -> PubKeyAlgorithm- -> HashAlgorithm- -> SignatureSalt- -> [SigSubPacket]- -> [SigSubPacket]- -> ByteString- -> Either SignError SignaturePayload-signEdDSAV6 algoName sigLen limbLen signer mode st pka ha salt has uhas payload = do- validateV6SaltSize ha salt- let normalizedPayload = normalizePayloadForSigTypeWith mode st payload- sig0 = SigV6 st pka ha salt has [] 0 (NE.fromList [MPI 0, MPI 0])- prehash =- hashWithSHA512- (BL.toStrict (finalPayload (SignaturePkt sig0) normalizedPayload))- sigBytes = signer prehash- if B.length sigBytes /= sigLen- then Left (SignProducedWrongLength algoName sigLen (B.length sigBytes))- else- let (r, s) = B.splitAt limbLen sigBytes- in (\left16 ->- SigV6- st- pka- ha- salt- has- uhas- left16- (NE.fromList [MPI (os2ip r), MPI (os2ip s)]))- <$> first SignBackendError (left16FromHashPrefix prehash)--signUserIDwithRSA :: SomePKPayload -- ^ public key "payload" of user ID being signed- -> UserId -- ^ user ID being signed- -> [SigSubPacket] -- ^ hashed signature subpackets- -> [SigSubPacket] -- ^ unhashed signature subpackets- -> RSATypes.PrivateKey -- ^ RSA signing key- -> Either SignError SignaturePayload-signUserIDwithRSA = signCertificationWithRSA PositiveCert--signCertificationWithRSA ::- SigType -- ^ certification type (GenericCert, PersonaCert, CasualCert, PositiveCert)- -> SomePKPayload -- ^ public key "payload" of user ID being signed- -> UserId -- ^ user ID being signed- -> [SigSubPacket] -- ^ hashed signature subpackets- -> [SigSubPacket] -- ^ unhashed signature subpackets- -> RSATypes.PrivateKey -- ^ RSA signing key- -> Either SignError SignaturePayload-signCertificationWithRSA st pkp uid hsigsubs usigsubs prv- | st `elem` [GenericCert, PersonaCert, CasualCert, PositiveCert] = do- let payloadToSign = BL.toStrict (finalPayload (SignaturePkt uidsigp) uidpayload)- uidsigp'- <$> left16FromSignedPayloadForSign SHA512 payloadToSign- <*> first- (SignBackendError . show)- (P15.sign- Nothing- (Just CHA.SHA512)- prv- payloadToSign)- | otherwise =- Left (SignUnsupportedCertificationType st)- where- uidpayload =- runPut- (sequence_- [putKeyforSigning (PublicKeyPkt pkp), putUforSigning (toPkt uid)])- uidsigp =- SigV4 st RSA SHA512 hsigsubs usigsubs 0 (NE.fromList [MPI 0])- uidsigp' left16 us =- SigV4- st- RSA- SHA512- hsigsubs- usigsubs- left16- (NE.fromList [MPI (os2ip us)])--signDirectKeyWithRSA ::- SigType -- ^ key-scoped signature type (SignatureDirectlyOnAKey or KeyRevocationSig)- -> SomePKPayload -- ^ primary key "payload" being signed- -> [SigSubPacket] -- ^ hashed signature subpackets- -> [SigSubPacket] -- ^ unhashed signature subpackets- -> RSATypes.PrivateKey -- ^ RSA signing key- -> Either SignError SignaturePayload-signDirectKeyWithRSA st pkp hsigsubs usigsubs prv- | st `elem` [SignatureDirectlyOnAKey, KeyRevocationSig] =- signDataWithRSA st prv hsigsubs usigsubs keypayload- | otherwise =- Left (SignUnsupportedKeySignatureType st)- where- keypayload = runPut (putKeyforSigning (PublicKeyPkt pkp))--signKeyRevocationWithRSA :: SomePKPayload -- ^ primary key "payload" being revoked- -> [SigSubPacket] -- ^ hashed signature subpackets- -> [SigSubPacket] -- ^ unhashed signature subpackets- -> RSATypes.PrivateKey -- ^ RSA signing key- -> Either SignError SignaturePayload-signKeyRevocationWithRSA = signDirectKeyWithRSA KeyRevocationSig--signSubkeyRevocationWithRSA :: SomePKPayload -- ^ primary key "payload"- -> SomePKPayload -- ^ public subkey "payload" being revoked- -> [SigSubPacket] -- ^ hashed signature subpackets- -> [SigSubPacket] -- ^ unhashed signature subpackets- -> RSATypes.PrivateKey -- ^ RSA signing key- -> Either SignError SignaturePayload-signSubkeyRevocationWithRSA pkp subpkp hsigsubs usigsubs prv =- signDataWithRSA SubkeyRevocationSig prv hsigsubs usigsubs subkeypayload- where- subkeypayload =- runPut- (sequence_- [ putKeyforSigning (PublicKeyPkt pkp)- , putKeyforSigning (PublicSubkeyPkt subpkp)- ])--signCertRevocationWithRSA :: SomePKPayload -- ^ primary key "payload"- -> UserId -- ^ user ID certification being revoked- -> [SigSubPacket] -- ^ hashed signature subpackets- -> [SigSubPacket] -- ^ unhashed signature subpackets- -> RSATypes.PrivateKey -- ^ RSA signing key- -> Either SignError SignaturePayload-signCertRevocationWithRSA pkp uid hsigsubs usigsubs prv =- signDataWithRSA CertRevocationSig prv hsigsubs usigsubs certpayload- where- certpayload =- runPut- (sequence_- [putKeyforSigning (PublicKeyPkt pkp), putUforSigning (toPkt uid)])--crossSignSubkeyWithRSA :: SomePKPayload -- ^ public key "payload" of key being signed- -> SomePKPayload -- ^ public subkey "payload" of key being signed- -> [SigSubPacket] -- ^ hashed signature subpackets for binding sig- -> [SigSubPacket] -- ^ unhashed signature subpackets for binding sig- -> [SigSubPacket] -- ^ hashed signature subpackets for embedded sig- -> [SigSubPacket] -- ^ unhashed signature subpackets for embedded sig- -> RSATypes.PrivateKey -- ^ RSA signing key- -> RSATypes.PrivateKey -- ^ RSA signing subkey- -> Either SignError SignaturePayload-crossSignSubkeyWithRSA pkp subpkp subhsigsubs subusigsubs embhsigsubs embusigsubs prv ssb = do- let embPayloadToSign = BL.toStrict (finalPayload (SignaturePkt embsigp) subkeypayload)- subPayloadToSign = BL.toStrict (finalPayload (SignaturePkt subsigp) subkeypayload)- (\embleft16 subleft16 embsig subsig ->- subsigp' (embsigp' embleft16 embsig) subleft16 subsig)- <$> left16FromSignedPayloadForSign SHA512 embPayloadToSign- <*> left16FromSignedPayloadForSign SHA512 subPayloadToSign- <*> first- (SignBackendError . show)- (P15.sign- Nothing- (Just CHA.SHA512)- ssb- embPayloadToSign)- <*> first- (SignBackendError . show)- (P15.sign- Nothing- (Just CHA.SHA512)- prv- subPayloadToSign)- where- subkeypayload =- runPut- (sequence_- [ putKeyforSigning (PublicKeyPkt pkp)- , putKeyforSigning (PublicSubkeyPkt subpkp)- ])- embsigp =- SigV4- PrimaryKeyBindingSig- RSA- SHA512- embhsigsubs- embusigsubs- 0- (NE.fromList [MPI 0])- embsigp' left16 es =- SigV4- PrimaryKeyBindingSig- RSA- SHA512- embhsigsubs- embusigsubs- left16- (NE.fromList [MPI (os2ip es)])- subsigp =- SigV4 SubkeyBindingSig RSA SHA512 subhsigsubs [] 0 (NE.fromList [MPI 0])- sspes es = SigSubPacket False (EmbeddedSignature es)- subsigp' es left16 ss =- SigV4- SubkeyBindingSig- RSA- SHA512- subhsigsubs- (sspes es : subusigsubs)- left16- (NE.fromList [MPI (os2ip ss)])--signRSAV4Core ::- TextNormalizationMode- -> SigType- -> HashAlgorithm- -> [SigSubPacket]- -> [SigSubPacket]- -> RSATypes.PrivateKey- -> ByteString- -> Either SignError SignaturePayload-signRSAV4Core mode st ha has uhas prv payload =- (\left16 ss -> SigV4 st RSA ha has uhas left16 (NE.fromList [MPI (os2ip ss)]))- <$> left16FromSignedPayloadForSign ha payloadToSign- <*> rsaPKCS15Sign ha prv payloadToSign- where- sig0 = SigV4 st RSA ha has [] 0 (NE.fromList [MPI 0])- payloadToSign = BL.toStrict (finalPayload (SignaturePkt sig0) (normalizePayloadForSigTypeWith mode st payload))--signRSAV6Core ::- TextNormalizationMode- -> SigType- -> HashAlgorithm- -> SignatureSalt- -> [SigSubPacket]- -> [SigSubPacket]- -> RSATypes.PrivateKey- -> ByteString- -> Either SignError SignaturePayload-signRSAV6Core mode st ha salt has uhas prv payload = do- validateV6SaltSize ha salt- (\left16 sigBytes -> SigV6 st RSA ha salt has uhas left16 (NE.fromList [MPI (os2ip sigBytes)]))- <$> left16FromSignedPayloadForSign ha payloadToSign- <*> rsaPKCS15Sign ha prv payloadToSign- where- sig0 = SigV6 st RSA ha salt has [] 0 (NE.fromList [MPI 0])- payloadToSign = BL.toStrict (finalPayload (SignaturePkt sig0) (normalizePayloadForSigTypeWith mode st payload))--signDataWithRSA ::- SigType- -> RSATypes.PrivateKey- -> [SigSubPacket]- -> [SigSubPacket]- -> ByteString- -> Either SignError SignaturePayload-signDataWithRSA st prv has uhas payload =- signRSAV4Core CleartextCompat st SHA512 has uhas prv payload--signDataWithRSAV6 ::- SigType- -> SignatureSalt- -> RSATypes.PrivateKey- -> [SigSubPacket]- -> [SigSubPacket]- -> ByteString- -> Either SignError SignaturePayload-signDataWithRSAV6 st salt prv has uhas payload =- signRSAV6Core CleartextCompat st SHA512 salt has uhas prv payload---- FIXME: clean this up-ed25519Params, ed25519LegacyParams, ed448Params :: (String, Int, Int, PubKeyAlgorithm)-ed25519Params = ("Ed25519", 64, 32, PKA.Ed25519)-ed25519LegacyParams = ("Ed25519Legacy", 64, 32, PKA.EdDSA)-ed448Params = ("Ed448", 114, 57, PKA.Ed448)--signDataWithEdDSAV4Generic ::- (String, Int, Int, PubKeyAlgorithm)- -> (B.ByteString -> B.ByteString)- -> TextNormalizationMode- -> HashAlgorithm- -> SigType- -> [SigSubPacket]- -> [SigSubPacket]- -> ByteString- -> Either SignError SignaturePayload-signDataWithEdDSAV4Generic (algoName, sigLen, limbLen, pka) signer mode ha st has uhas payload =- signEdDSAV4 algoName sigLen limbLen signer mode st pka ha has uhas payload--signDataWithEdDSAV6Generic ::- (String, Int, Int, PubKeyAlgorithm)- -> (B.ByteString -> B.ByteString)- -> TextNormalizationMode- -> HashAlgorithm- -> SigType- -> SignatureSalt- -> [SigSubPacket]- -> [SigSubPacket]- -> ByteString- -> Either SignError SignaturePayload-signDataWithEdDSAV6Generic (algoName, sigLen, limbLen, pka) signer mode ha st salt has uhas payload =- signEdDSAV6 algoName sigLen limbLen signer mode st pka ha salt has uhas payload--signDataWithEd25519 ::- SigType- -> Ed25519.SecretKey- -> [SigSubPacket]- -> [SigSubPacket]- -> ByteString- -> Either SignError SignaturePayload-signDataWithEd25519 st sk has uhas payload =- signDataWithEdDSAV4Generic ed25519Params (ed25519Signer sk) CleartextCompat SHA512 st has uhas payload--signDataWithEd25519Legacy ::- SigType- -> Ed25519.SecretKey- -> [SigSubPacket]- -> [SigSubPacket]- -> ByteString- -> Either SignError SignaturePayload-signDataWithEd25519Legacy st sk has uhas payload =- signDataWithEdDSAV4Generic ed25519LegacyParams (ed25519Signer sk) CleartextCompat SHA512 st has uhas payload--signDataWithEd25519V6 ::- SigType- -> SignatureSalt- -> Ed25519.SecretKey- -> [SigSubPacket]- -> [SigSubPacket]- -> ByteString- -> Either SignError SignaturePayload-signDataWithEd25519V6 st salt sk has uhas payload =- signDataWithEdDSAV6Generic ed25519Params (ed25519Signer sk) CleartextCompat SHA512 st salt has uhas payload--signDataWithEd448 ::- SigType- -> Ed448.SecretKey- -> [SigSubPacket]- -> [SigSubPacket]- -> ByteString- -> Either SignError SignaturePayload-signDataWithEd448 st sk has uhas payload =- signDataWithEdDSAV4Generic ed448Params (ed448Signer sk) CleartextCompat SHA512 st has uhas payload--signDataWithEd448V6 ::- SigType- -> SignatureSalt- -> Ed448.SecretKey- -> [SigSubPacket]- -> [SigSubPacket]- -> ByteString- -> Either SignError SignaturePayload-signDataWithEd448V6 st salt sk has uhas payload =- signDataWithEdDSAV6Generic ed448Params (ed448Signer sk) CleartextCompat SHA512 st salt has uhas payload---- | Builder-based signature creation for RSA--- --- Example usage:--- builder <- sigBuilderInit BinarySig RSA SHA512--- builder' <- addHashedSubs hashedSubpackets builder--- builder'' <- addUnhashedSubs unhashedSubpackets builder'--- sig <- signDataWithRSABuilder builder'' rsaPrivateKey payload-signDataWithRSABuilder ::- SigBuilder Codec.Encryption.OpenPGP.Types.Unhashed Codec.Encryption.OpenPGP.Types.V4Sig 'PKA.RSA- -> RSATypes.PrivateKey- -> ByteString- -> Either SignError SignaturePayload-signDataWithRSABuilder builder prv payload =- signRSAV4Core- (sbTextNormMode builder)- (sbSigType builder)- (sbHashAlgo builder)- (sbHashedSubs builder)- (sbUnhashedSubs builder)- prv- payload---- | Builder-based signature creation for RSA (v6)-signDataWithRSAV6Builder ::- SigBuilder Codec.Encryption.OpenPGP.Types.Unhashed Codec.Encryption.OpenPGP.Types.V6Sig 'PKA.RSA- -> RSATypes.PrivateKey- -> ByteString- -> Either SignError SignaturePayload-signDataWithRSAV6Builder builder prv payload =- signRSAV6Core- (sbTextNormMode builder)- (sbSigType builder)- (sbHashAlgo builder)- (sbSalt builder)- (sbHashedSubs builder)- (sbUnhashedSubs builder)- prv- payload---- | Builder-based signature creation for Ed25519 (v4)------ Example usage:--- builder <- sigBuilderInit BinarySig Ed25519 SHA512--- builder' <- addHashedSubs hashedSubpackets builder--- builder'' <- addUnhashedSubs unhashedSubpackets builder'--- sig <- signDataWithEd25519Builder builder'' ed25519PrivateKey payload-signDataWithEd25519Builder ::- SigBuilder Codec.Encryption.OpenPGP.Types.Unhashed Codec.Encryption.OpenPGP.Types.V4Sig 'PKA.Ed25519- -> Ed25519.SecretKey- -> ByteString- -> Either SignError SignaturePayload-signDataWithEd25519Builder builder sk payload =- signDataWithEdDSAV4Generic ed25519Params (ed25519Signer sk)- (sbTextNormMode builder) (sbHashAlgo builder) (sbSigType builder)- (sbHashedSubs builder) (sbUnhashedSubs builder) payload---- | Builder-based signature creation for Ed25519 (v6)------ Example usage:--- builder <- sigBuilderInitV6 BinarySig Ed25519 SHA512 salt--- builder' <- addHashedSubs hashedSubpackets builder--- builder'' <- addUnhashedSubs unhashedSubpackets builder'--- sig <- signDataWithEd25519V6Builder builder'' ed25519PrivateKey payload-signDataWithEd25519V6Builder ::- SigBuilder Codec.Encryption.OpenPGP.Types.Unhashed Codec.Encryption.OpenPGP.Types.V6Sig 'PKA.Ed25519- -> Ed25519.SecretKey- -> ByteString- -> Either SignError SignaturePayload-signDataWithEd25519V6Builder builder sk payload =- signDataWithEdDSAV6Generic ed25519Params (ed25519Signer sk)- (sbTextNormMode builder) (sbHashAlgo builder) (sbSigType builder)- (sbSalt builder) (sbHashedSubs builder) (sbUnhashedSubs builder) payload---- | Builder-based signature creation for Ed448 (v4)------ Example usage:--- builder <- sigBuilderInit BinarySig Ed448 SHA512--- builder' <- addHashedSubs hashedSubpackets builder--- builder'' <- addUnhashedSubs unhashedSubpackets builder'--- sig <- signDataWithEd448Builder builder'' ed448PrivateKey payload-signDataWithEd448Builder ::- SigBuilder Codec.Encryption.OpenPGP.Types.Unhashed Codec.Encryption.OpenPGP.Types.V4Sig 'PKA.Ed448- -> Ed448.SecretKey- -> ByteString- -> Either SignError SignaturePayload-signDataWithEd448Builder builder sk payload =- signDataWithEdDSAV4Generic ed448Params (ed448Signer sk)- (sbTextNormMode builder) (sbHashAlgo builder) (sbSigType builder)- (sbHashedSubs builder) (sbUnhashedSubs builder) payload---- | Builder-based signature creation for Ed448 (v6)-signDataWithEd448V6Builder ::- SigBuilder Codec.Encryption.OpenPGP.Types.Unhashed Codec.Encryption.OpenPGP.Types.V6Sig 'PKA.Ed448- -> Ed448.SecretKey- -> ByteString- -> Either SignError SignaturePayload-signDataWithEd448V6Builder builder sk payload =- signDataWithEdDSAV6Generic ed448Params (ed448Signer sk)- (sbTextNormMode builder) (sbHashAlgo builder) (sbSigType builder)- (sbSalt builder) (sbHashedSubs builder) (sbUnhashedSubs builder) payload---- | Algorithm-agnostic signature builder dispatcher (Phase 2)--- --- Dispatches to the appropriate signing function based on the private key type.--- The private key type (PrivateKeyFor algo) encodes the algorithm at the type level,--- allowing compile-time verification that the key and builder algorithm match.------ Example usage:-class BuilderSigningAlgorithm (algo :: PKA.PubKeyAlgorithm) where- signDataWithAlgorithmicBuilderImpl- :: SigBuilder Codec.Encryption.OpenPGP.Types.Unhashed Codec.Encryption.OpenPGP.Types.V4Sig algo- -> PrivateKeyFor algo- -> ByteString- -> Either SignError SignaturePayload--instance BuilderSigningAlgorithm 'PKA.RSA where- signDataWithAlgorithmicBuilderImpl builder (SP.RSAPrivateKey prv) payload =- signDataWithRSABuilder builder prv payload--instance BuilderSigningAlgorithm 'PKA.Ed25519 where- signDataWithAlgorithmicBuilderImpl builder (SP.Ed25519PrivateKey sk) payload =- signDataWithEd25519Builder builder sk payload--instance BuilderSigningAlgorithm 'PKA.Ed448 where- signDataWithAlgorithmicBuilderImpl builder (SP.Ed448PrivateKey sk) payload =- signDataWithEd448Builder builder sk payload---- | Catch-all instance that produces a compile-time error for any algorithm--- that is not supported by the algorithmic builder (e.g. DSA, ECDSA).-instance {-# OVERLAPPABLE #-}- TypeError- ( 'Text "signDataWithAlgorithmicBuilder does not support this algorithm."- ':$$: 'Text "Supported algorithms: RSA, Ed25519, Ed448."- ':$$: 'Text "For DSA or ECDSA, use signDataWith{DSA,ECDSA}Builder directly."- )- => BuilderSigningAlgorithm algo where- signDataWithAlgorithmicBuilderImpl = error "unreachable: TypeError fires at compile time"--signDataWithAlgorithmicBuilder- :: forall (algo :: PKA.PubKeyAlgorithm)- . BuilderSigningAlgorithm algo- => SigBuilder Codec.Encryption.OpenPGP.Types.Unhashed Codec.Encryption.OpenPGP.Types.V4Sig algo- -> PrivateKeyFor algo- -> ByteString- -> Either SignError SignaturePayload-signDataWithAlgorithmicBuilder =- signDataWithAlgorithmicBuilderImpl+{-# LANGUAGE ConstraintKinds #-}+-- Signatures.hs: OpenPGP (RFC9580) signature verification+-- Copyright © 2012-2026 Clint Adams+-- This software is released under the terms of the Expat license.+-- (See the LICENSE file).+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++module Codec.Encryption.OpenPGP.Signatures+ ( SignError (..)+ , renderSignError+ , CertificationState (..)+ , certificationStateAt+ , VerificationError (..)+ , renderVerificationError+ , verifySigWith+ , verifyAgainstKeyring+ , verifyAgainstKeys+ , verifyAgainstKeysWithPolicy+ , verifyAgainstPKPs+ , verifyAgainstPKPsWithPolicy+ , verifyTKWith+ , verifyUnknownTKWith+ , signCertificationWithRSA+ , signDirectKeyWithRSA+ , signKeyRevocationWithRSA+ , signSubkeyRevocationWithRSA+ , signCertRevocationWithRSA+ , signUserIDwithRSA+ , crossSignSubkeyWithRSA+ , signDataWithEd25519+ , signDataWithEd25519Legacy+ , signDataWithEd25519V6+ , signDataWithEd448+ , signDataWithEd448V6+ , signDataWithRSA+ , signDataWithRSAV6++ -- * Builder-based API (Phase 2)+ , signDataWithRSABuilder+ , signDataWithRSAV6Builder+ , signDataWithEd25519Builder+ , signDataWithEd25519V6Builder+ , signDataWithEd448Builder+ , signDataWithEd448V6Builder+ , signDataWithAlgorithmicBuilder++ -- * Text normalization mode+ , TextNormalizationMode (..)+ ) where++import Control.Applicative ((<|>))+import Control.Error.Util (hush)+import Control.Lens ((&), (^.), _1)+import Control.Monad (liftM2, when)+import Crypto.Error (eitherCryptoError)+import Crypto.Hash (hashWith)+import qualified Crypto.Hash.Algorithms as CHA+import Crypto.Number.Serialize (i2osp, os2ip)+import qualified Crypto.PubKey.DSA as DSA+import qualified Crypto.PubKey.ECC.ECDSA as ECDSA+import qualified Crypto.PubKey.Ed25519 as Ed25519+import qualified Crypto.PubKey.Ed448 as Ed448+import qualified Crypto.PubKey.RSA.PKCS15 as P15+import qualified Crypto.PubKey.RSA.Types as RSATypes+import Data.Bifunctor (first)+import Data.Binary.Put (runPut)+import qualified Data.ByteArray as BA+import qualified Data.ByteString as B+import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Lazy as BL+import Data.Either (isRight, lefts, rights)+import Data.Function (on)+import Data.IxSet.Typed ((@=))+import qualified Data.IxSet.Typed as IxSet+import Data.List (find, intercalate, nub)+import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NE+import qualified Data.Map.Strict as Map+import Data.Maybe (isJust, mapMaybe)+import qualified Data.Set as Set+import Data.Text (Text)+import Data.Time.Clock (UTCTime (..), addUTCTime, diffUTCTime)+import Data.Time.Clock.POSIX (posixSecondsToUTCTime)+import Data.Word (Word16, Word8)+import GHC.TypeLits (ErrorMessage (..), TypeError)++import Codec.Encryption.OpenPGP.Expirations+ ( isPKTimeValidWithSelfSignatures+ , keyStateAt+ , keyStateValid+ )+import Codec.Encryption.OpenPGP.Fingerprint+ ( eightOctetKeyID+ , fingerprint+ )+import Codec.Encryption.OpenPGP.Internal+ ( PktStreamContext (..)+ , emptyPSC+ , issuer+ , issuerFP+ )+import Codec.Encryption.OpenPGP.Ontology+ ( isCertRevocationSig+ , isRevocationKeySSP+ , isRevokerP+ , isSubkeyBindingSig+ , isSubkeyRevocation+ )+import Codec.Encryption.OpenPGP.Policy+ ( VerificationPolicy (..)+ , VerificationPolicyAction (..)+ , applyVerificationPolicy+ , defaultVerificationPolicy+ , isVerificationError+ , isVerificationWarning+ , signatureV6SaltSizeForHashAlgorithm+ )+import Codec.Encryption.OpenPGP.SerializeForSigs+ ( payloadForSig+ , putKeyforSigning+ , putPartialSigforSigning+ , putSigTrailer+ , putUforSigning+ )+import Codec.Encryption.OpenPGP.SignatureQualities+ ( sigCT+ , sigHA+ , sigPKA+ , sigType+ , signatureHashedSubpacketsKnown+ , signatureSubpacketListsKnown+ )+import Codec.Encryption.OpenPGP.Subpackets+ ( PrivateKeyFor+ , SigBuilder+ , TextNormalizationMode (..)+ , sbHashAlgo+ , sbHashedSubs+ , sbSalt+ , sbSigType+ , sbTextNormMode+ , sbUnhashedSubs+ )+import qualified Codec.Encryption.OpenPGP.Subpackets as SP+import Codec.Encryption.OpenPGP.Types+import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as PKA+import Codec.Encryption.OpenPGP.Types.Internal.Pkt+ ( VerificationWarning (..)+ )+import Data.Conduit.OpenPGP.Keyring.Instances ()++data VerificationError+ = IssuerSubpacketMismatch+ | IssuerSubpacketUncheckable String+ | IssuerKeyIdProhibitedInV6Signature+ | IssuerFingerprintSubpacketMismatch+ | UnsupportedCriticalSubpacket SigType+ | UnknownCriticalPacketInStream Word8+ | BrokenCriticalPacketInStream Word8 String+ | ExternalVerificationError String+ | NonSignaturePacket+ | UnexpectedSignaturePayloadShape+ | MissingHashAlgorithm+ | HashComputationFailed String+ | UnexpectedKeyVersion+ | SignatureHashUnsupportedByAlgorithm HashAlgorithm PubKeyAlgorithm+ | KeyRevoked+ | SigningKeyUnavailableAtSignatureTime+ | MissingIssuer+ | SigningKeyNotFound (Maybe EightOctetKeyId) (Maybe Fingerprint)+ | MultipleVerificationSuccesses Int+ | UnsupportedKeyType PubKeyAlgorithm+ | SignatureMismatch PubKeyAlgorithm Fingerprint+ | SignatureShapeMismatch PubKeyAlgorithm+ | SignatureEncodingInvalid PubKeyAlgorithm String+ | SignaturePolicyHashUnsupported HashAlgorithm+ | SignaturePolicyPKAMismatch PubKeyAlgorithm PubKeyAlgorithm+ | SignatureExpired+ | CandidateKeyFailures [VerificationError]+ | {- | An embedded primary-key back-signature (type 0x19) in a subkey+ binding signature failed to verify.+ -}+ InvalidSubkeyBackSignature VerificationError+ deriving (Eq, Show)++data CertificationState+ = CertificationNotYetKnown+ | CertificationActive+ | CertificationRevoked+ deriving (Eq, Show)++renderVerificationError :: VerificationError -> String+renderVerificationError IssuerSubpacketMismatch =+ "verification failed: issuer subpacket does not match the actual signer"+renderVerificationError (IssuerSubpacketUncheckable err) =+ "verification failed: issuer subpacket cannot be checked ("+ ++ err+ ++ ")"+renderVerificationError IssuerKeyIdProhibitedInV6Signature =+ "verification failed: Issuer Key ID subpacket is prohibited in v6 signatures"+renderVerificationError IssuerFingerprintSubpacketMismatch =+ "verification failed: issuer fingerprint subpacket does not match the actual signer"+renderVerificationError (UnsupportedCriticalSubpacket sigType) =+ "verification failed: unsupported critical hashed subpacket in "+ ++ show sigType+ ++ " signature"+renderVerificationError (UnknownCriticalPacketInStream t) =+ "verification failed: unknown critical packet type in packet sequence ("+ ++ show t+ ++ ")"+renderVerificationError (BrokenCriticalPacketInStream t err) =+ "verification failed: broken critical packet type "+ ++ show t+ ++ ": "+ ++ err+renderVerificationError (ExternalVerificationError err) = err+renderVerificationError NonSignaturePacket =+ "verification failed: non-signature packet encountered where signature was expected"+renderVerificationError UnexpectedSignaturePayloadShape =+ "verification failed: unexpected signature payload shape"+renderVerificationError MissingHashAlgorithm =+ "verification failed: signature payload is missing hash algorithm"+renderVerificationError (HashComputationFailed err) =+ "verification failed: hash computation error (" ++ err ++ ")"+renderVerificationError UnexpectedKeyVersion =+ "verification failed: signing key has unexpected version (only v4 and v6 are supported)"+renderVerificationError (SignatureHashUnsupportedByAlgorithm ha pka) =+ "verification failed: hash algorithm "+ ++ show ha+ ++ " is not supported by "+ ++ show pka+ ++ " signing backend"+renderVerificationError KeyRevoked =+ "verification failed: signing key is revoked"+renderVerificationError SigningKeyUnavailableAtSignatureTime =+ "verification failed: signing key was not valid at the signature creation time"+renderVerificationError MissingIssuer =+ "verification failed: signature is missing issuer information"+renderVerificationError (SigningKeyNotFound meoki mfp) =+ "verification failed: signing key not found in keyring"+ ++ issuerContext meoki mfp+renderVerificationError (MultipleVerificationSuccesses n) =+ "verification failed: multiple successful key matches ("+ ++ show n+ ++ ")"+renderVerificationError (UnsupportedKeyType pka) =+ "verification failed: unsupported public key algorithm for verification ("+ ++ show pka+ ++ ")"+renderVerificationError (SignatureMismatch pka fpr) =+ "verification failed: "+ ++ show pka+ ++ " signature mismatch (signer "+ ++ show fpr+ ++ ")"+renderVerificationError (SignatureShapeMismatch pka) =+ "verification failed: malformed "+ ++ show pka+ ++ " signature encoding"+renderVerificationError (SignatureEncodingInvalid pka err) =+ "verification failed: invalid "+ ++ show pka+ ++ " key/signature encoding ("+ ++ err+ ++ ")"+renderVerificationError (SignaturePolicyHashUnsupported ha) =+ "verification failed: unsupported signature hash policy ("+ ++ show ha+ ++ ")"+renderVerificationError (SignaturePolicyPKAMismatch sigPka keyPka) =+ "verification failed: signature public-key algorithm "+ ++ show sigPka+ ++ " does not match key algorithm "+ ++ show keyPka+renderVerificationError SignatureExpired =+ "verification failed: signature expired"+renderVerificationError (CandidateKeyFailures errs) =+ "verification failed: no candidate key validated the signature ("+ ++ intercalate "; " (nub (map renderVerificationError errs))+ ++ ")"+renderVerificationError (InvalidSubkeyBackSignature err) =+ "verification failed: embedded primary-key back-signature verification failed: "+ ++ renderVerificationError err++issuerContext+ :: Maybe EightOctetKeyId -> Maybe Fingerprint -> String+issuerContext meoki mfp =+ case (meoki, mfp) of+ (Nothing, Nothing) -> ""+ _ ->+ " (issuer-keyid="+ ++ maybe "unknown" show meoki+ ++ ", issuer-fingerprint="+ ++ maybe "unknown" show mfp+ ++ ")"++verificationError+ :: VerificationError -> Either VerificationError a+verificationError = Left++renderVerificationResult+ :: Either VerificationError a -> Either String a+renderVerificationResult = first renderVerificationError++data SignError+ = SignBackendError String+ | SignUnsupportedCertificationType SigType+ | SignUnsupportedKeySignatureType SigType+ | SignV6SaltSizeMismatch HashAlgorithm Word8 Int+ | SignProducedWrongLength String Int Int+ deriving (Eq, Show)++renderSignError :: SignError -> String+renderSignError (SignBackendError err) =+ "signature backend error: " ++ err+renderSignError (SignUnsupportedCertificationType st) =+ "unsupported certification signature type: "+ ++ show st+ ++ " (expected one of GenericCert/PersonaCert/CasualCert/PositiveCert)"+renderSignError (SignUnsupportedKeySignatureType st) =+ "unsupported key signature type: "+ ++ show st+ ++ " (expected SignatureDirectlyOnAKey or KeyRevocationSig)"+renderSignError (SignV6SaltSizeMismatch ha expected actual) =+ "v6 signature salt size mismatch for "+ ++ show ha+ ++ ": expected "+ ++ show expected+ ++ ", got "+ ++ show actual+renderSignError (SignProducedWrongLength algo expected actual) =+ algo+ ++ " produced a non-"+ ++ show expected+ ++ "-byte signature (got "+ ++ show actual+ ++ ")"++data VerifiableSignatureV where+ VerifiableSignatureV4+ :: SignaturePayloadV 'SigPayloadV4 -> VerifiableSignatureV+ VerifiableSignatureV6+ :: SignaturePayloadV 'SigPayloadV6 -> VerifiableSignatureV++fromSignaturePayloadVerifiableSignatureV+ :: SignaturePayload -> Maybe VerifiableSignatureV+fromSignaturePayloadVerifiableSignatureV sigPayload =+ case toSomeSignaturePayload sigPayload of+ SomeSignaturePayload (payload@SigPayloadV4Data {}) ->+ Just (VerifiableSignatureV4 payload)+ SomeSignaturePayload (payload@SigPayloadV6Data {}) ->+ Just (VerifiableSignatureV6 payload)+ _ -> Nothing++isVerifiableSignaturePayload :: SignaturePayload -> Bool+isVerifiableSignaturePayload = isJust . fromSignaturePayloadVerifiableSignatureV++toSignaturePayloadFromVerifiable+ :: VerifiableSignatureV -> SignaturePayload+toSignaturePayloadFromVerifiable (VerifiableSignatureV4 payload) =+ toSignaturePayload payload+toSignaturePayloadFromVerifiable (VerifiableSignatureV6 payload) =+ toSignaturePayload payload++fromPktEitherVerifiableSignatureV+ :: Pkt -> Either VerificationError VerifiableSignatureV+fromPktEitherVerifiableSignatureV (SignaturePkt sigPayload) =+ case fromSignaturePayloadVerifiableSignatureV sigPayload of+ Just verifiableSig -> Right verifiableSig+ Nothing ->+ verificationError UnexpectedSignaturePayloadShape+fromPktEitherVerifiableSignatureV _ =+ verificationError NonSignaturePacket++signaturePKAAndMPIsFromClass+ :: SomeSignatureV+ -> Either VerificationError (PubKeyAlgorithm, NonEmpty MPI)+signaturePKAAndMPIsFromClass =+ fmap (\(pka, _, mpis) -> (pka, mpis))+ . signatureVerificationMaterialFromClass++signatureLeft16FromClass+ :: SomeSignatureV -> Either VerificationError Word16+signatureLeft16FromClass =+ fmap (\(_, l16, _) -> l16)+ . signatureVerificationMaterialFromClass++signatureVerificationMaterialFromClass+ :: SomeSignatureV+ -> Either VerificationError (PubKeyAlgorithm, Word16, NonEmpty MPI)+signatureVerificationMaterialFromClass (SomeSignatureV typedSig) =+ case typedSig of+ SignatureV3Packet (SigPayloadV3Data _ _ _ pka _ l16 mpis) ->+ Right (pka, l16, mpis)+ SignatureV4Packet (SigPayloadV4Data _ pka _ _ _ l16 mpis) ->+ Right (pka, l16, mpis)+ SignatureV6Packet (SigPayloadV6Data _ pka _ _ _ _ l16 mpis) ->+ Right (pka, l16, mpis)+ _ ->+ verificationError UnexpectedSignaturePayloadShape++verifySigWith+ :: VerificationPolicy+ -> ( Pkt+ -> Maybe UTCTime+ -> ByteString+ -> Either VerificationError Verification+ )+ -> Pkt+ -> PktStreamContext+ -> Maybe UTCTime+ -> Either VerificationError Verification+verifySigWith policy vf sig@(SignaturePkt _) state mt =+ case fromPktEitherVerifiableSignatureV sig of+ Right verifiableSig ->+ let (st, hs, us, checkSubpacket, checkUnhashedSubpackets) =+ verifiableSignatureVerificationInputs verifiableSig+ in checkUnhashedSubpackets us+ *> verifyWithSubpacketChecks+ policy+ vf+ sig+ state+ mt+ st+ hs+ checkSubpacket+ Left err ->+ Left err+ where+ checkV4Subpacket signer i@Issuer {} = checkIssuerSubpacket (eightOctetKeyID signer) i+ checkV4Subpacket signer i@IssuerFingerprint {} =+ checkIssuerFingerprintSubpacket+ PKA.IssuerFingerprintV4+ (fingerprint signer)+ i+ checkV4Subpacket _ _ = Right True+ -- RFC 9580 §5.2.3.35: v6 signatures MUST NOT include an Issuer Key ID subpacket.+ -- Treat any such subpacket as a verification error rather than merely uncheckable.+ checkV6Subpacket _ Issuer {} =+ case applyVerificationPolicy+ (vpLegacyIssuerKeyIdInV6 policy)+ "Issuer Key ID subpacket is prohibited in v6 signatures" of+ Left err -> verificationError IssuerKeyIdProhibitedInV6Signature+ Right warn -> Right True -- We don't have a warning type for this yet+ checkV6Subpacket signer i@IssuerFingerprint {} =+ checkIssuerFingerprintSubpacket+ PKA.IssuerFingerprintV6+ (fingerprint signer)+ i+ checkV6Subpacket _ _ = Right True+ rejectV6UnhashedIssuer (SigSubPacket _ Issuer {}) =+ case applyVerificationPolicy+ (vpLegacyIssuerKeyIdInV6 policy)+ "Issuer Key ID subpacket is prohibited in v6 signatures" of+ Left err -> verificationError IssuerKeyIdProhibitedInV6Signature+ Right warn -> Right () -- We don't have a warning type for this yet+ rejectV6UnhashedIssuer _ = Right ()+ verifiableSignatureVerificationInputs+ :: VerifiableSignatureV+ -> ( SigType+ , [SigSubPacket]+ , [SigSubPacket]+ , SomePKPayload+ -> SigSubPacketPayload+ -> Either VerificationError Bool+ , [SigSubPacket] -> Either VerificationError ()+ )+ verifiableSignatureVerificationInputs+ (VerifiableSignatureV4 (SigPayloadV4Data st _ _ hs us _ _)) =+ (st, hs, us, checkV4Subpacket, const (Right ()))+ verifiableSignatureVerificationInputs+ (VerifiableSignatureV6 (SigPayloadV6Data st _ _ _ hs us _ _)) =+ (st, hs, us, checkV6Subpacket, mapM_ rejectV6UnhashedIssuer)+verifySigWith _ _ _ _ _ =+ verificationError NonSignaturePacket++verifyWithSubpacketChecks+ :: VerificationPolicy+ -> ( Pkt+ -> Maybe UTCTime+ -> ByteString+ -> Either VerificationError Verification+ )+ -> Pkt+ -> PktStreamContext+ -> Maybe UTCTime+ -> SigType+ -> [SigSubPacket]+ -> ( SomePKPayload+ -> SigSubPacketPayload+ -> Either VerificationError Bool+ )+ -> Either VerificationError Verification+verifyWithSubpacketChecks policy vf sig state mt sigType hashedSubpackets checkSubpacket = do+ mapM_+ (rejectUnsupportedCriticalSubpacket policy sigType)+ hashedSubpackets+ v <- vf sig mt (payloadForSig sigType state)+ mapM_+ (checkSubpacket (v ^. verificationSigner) . _sspPayload)+ hashedSubpackets+ warnings <-+ if sigType == SubkeyBindingSig+ then verifySubkeyBackSignatures policy state mt hashedSubpackets+ else Right []+ isSignatureExpired sig mt+ *> pure+ ( v+ { _verificationWarnings =+ _verificationWarnings v ++ warnings+ }+ )++{- | Verify embedded primary-key back-signatures (PrimaryKeyBindingSig, 0x19)+found in the hashed subpackets of a SubkeyBindingSig.++Per RFC 9580 §5.2.3.3, a signing-capable subkey MUST include an embedded+PrimaryKeyBindingSig (0x19) made by the subkey. This requirement is+enforced strictly for v6 subkeys. For v4 subkeys we still verify any+embedded back-sigs that are present, but do not reject a missing one —+real-world v4 signing subkeys predate the strict cross-certification+mandate and widespread interoperability requires accepting them.+-}+verifySubkeyBackSignatures+ :: VerificationPolicy+ -> PktStreamContext+ -> Maybe UTCTime+ -> [SigSubPacket]+ -> Either VerificationError [VerificationWarning]+verifySubkeyBackSignatures policy state mt hashedSubpackets = do+ subkeyPKP <-+ maybe+ (verificationError NonSignaturePacket)+ Right+ (subkeyPKPFromPkt (lastSubkey state))+ let embeddedSigs =+ [ sp+ | SigSubPacket _ (EmbeddedSignature sp) <- hashedSubpackets+ ]+ isSigningCapable =+ any+ ( \(SigSubPacket _ payload) ->+ case payload of+ KeyFlags flags -> SignDataKey `Set.member` flags+ _ -> False+ )+ hashedSubpackets+ isV6Subkey = _keyVersion subkeyPKP == V6+ case embeddedSigs of+ [] ->+ -- Require back-sig only for v6 signing subkeys (RFC 9580 §5.2.3.3).+ if isSigningCapable && isV6Subkey+ then case applyVerificationPolicy+ (vpMissingSubkeyBackSignature policy)+ "Missing subkey back-signature (v6 signing subkey)" of+ Left err ->+ verificationError (SignaturePolicyHashUnsupported DeprecatedMD5) -- We'll need a better error type+ Right warn -> Right [MissingSubkeyBackSignatureWarning]+ else Right []+ _ ->+ -- Always verify back-sigs that are present, regardless of key version.+ mapM_ (verifyOneBackSig policy subkeyPKP) embeddedSigs+ *> Right []+ where+ verifyOneBackSig vp subkeyPKP embSigPayload = do+ let embSigPkt = SignaturePkt embSigPayload+ backSigContext =+ emptyPSC+ { lastPrimaryKey = lastPrimaryKey state+ , lastSubkey = lastSubkey state+ }+ case verifyAgainstKeyWithPolicy+ vp+ subkeyPKP+ embSigPkt+ mt+ (payloadForSig PrimaryKeyBindingSig backSigContext) of+ Left err -> verificationError (InvalidSubkeyBackSignature err)+ Right _ -> Right ()++rejectUnsupportedCriticalSubpacket+ :: VerificationPolicy+ -> SigType+ -> SigSubPacket+ -> Either VerificationError ()+rejectUnsupportedCriticalSubpacket policy sigType (SigSubPacket isCritical payload)+ | not isCritical = Right ()+ | not (isBindingSignatureType sigType) = Right ()+ | otherwise =+ case payload of+ UserDefinedSigSub {} ->+ case applyVerificationPolicy+ (vpUnsupportedCriticalSubpacket policy)+ ( "Unsupported critical subpacket in "+ ++ show sigType+ ++ " signature"+ ) of+ Left err -> verificationError (UnsupportedCriticalSubpacket sigType)+ Right warn -> Right () -- We don't have a warning type for this yet+ OtherSigSub {} ->+ case applyVerificationPolicy+ (vpUnsupportedCriticalSubpacket policy)+ ( "Unsupported critical subpacket in "+ ++ show sigType+ ++ " signature"+ ) of+ Left err -> verificationError (UnsupportedCriticalSubpacket sigType)+ Right warn -> Right ()+ _ -> Right ()++isBindingSignatureType :: SigType -> Bool+isBindingSignatureType SubkeyBindingSig = True+isBindingSignatureType PrimaryKeyBindingSig = True+isBindingSignatureType _ = False++checkIssuerSubpacket+ :: Either String EightOctetKeyId+ -> SigSubPacketPayload+ -> Either VerificationError Bool+checkIssuerSubpacket (Right signer) (Issuer i)+ | signer == i = Right True+ | otherwise = verificationError IssuerSubpacketMismatch+checkIssuerSubpacket (Left err) (Issuer _) =+ verificationError (IssuerSubpacketUncheckable err)+checkIssuerSubpacket _ _ = Right True++checkIssuerFingerprintSubpacket+ :: IssuerFingerprintVersion+ -> Fingerprint+ -> SigSubPacketPayload+ -> Either VerificationError Bool+checkIssuerFingerprintSubpacket expectedVersion signer (IssuerFingerprint kv i)+ | kv /= expectedVersion =+ verificationError IssuerFingerprintSubpacketMismatch+ | signer == i = Right True+ | otherwise =+ verificationError IssuerFingerprintSubpacketMismatch+checkIssuerFingerprintSubpacket _ _ _ = Right True++verifyTKWith+ :: ( Pkt+ -> PktStreamContext+ -> Maybe UTCTime+ -> Either VerificationError Verification+ )+ -> Maybe UTCTime+ -> TK k+ -> Either VerificationError (TK k)+verifyTKWith vsf mt tk = do+ verifiedUnknown <- verifyUnknownTKWith vsf mt (tkToUnknown tk)+ let typedSubkeys =+ Map.fromList [(keyPktToPkt kp, kp) | (kp, _) <- tk ^. tkSubs]+ verifiedTypedSubkeys =+ mapMaybe+ ( \(pkt, sigs) -> (\kp -> (kp, sigs)) <$> Map.lookup pkt typedSubkeys+ )+ (verifiedUnknown ^. tkuSubs)+ pure+ TK+ { _tkPrimaryKey = tk ^. tkPrimaryKey+ , _tkRevs = verifiedUnknown ^. tkuRevs+ , _tkUIDs = verifiedUnknown ^. tkuUIDs+ , _tkUAts = verifiedUnknown ^. tkuUAts+ , _tkSubs = verifiedTypedSubkeys+ }++verifyUnknownTKWith+ :: ( Pkt+ -> PktStreamContext+ -> Maybe UTCTime+ -> Either VerificationError Verification+ )+ -> Maybe UTCTime+ -> TKUnknown+ -> Either VerificationError TKUnknown+verifyUnknownTKWith vsf mt tk = do+ revokers <- checkRevokers tk+ revs <- checkKeyRevocations revokers tk+ let uids = filter (not . null . snd) . checkUidSigs $ tk ^. tkuUIDs+ let uats = filter (not . null . snd) . checkUAtSigs $ tk ^. tkuUAts+ let subs = concatMap checkSub $ tk ^. tkuSubs+ return (TKUnknown (tk ^. tkuKey) revs uids uats subs)+ where+ checkRevokers =+ Right+ . concat+ . rights+ . map verifyRevoker+ . filter isRevokerP+ . _tkuRevs+ checkKeyRevocations+ :: [(PubKeyAlgorithm, Fingerprint)]+ -> TKUnknown+ -> Either VerificationError [SignaturePayload]+ checkKeyRevocations rs k =+ Prelude.sequence+ . concatMap (filterRevs rs)+ . rights+ . map (liftM2 fmap (,) vSig)+ $ k+ ^. tkuRevs+ checkUidSigs+ :: [(Text, [SignaturePayload])] -> [(Text, [SignaturePayload])]+ checkUidSigs =+ map+ ( \(uid, sps) ->+ let verified = rights . map (\sp -> fmap ((,) sp) (vUid (uid, sp))) $ sps+ in (uid, retainNonRevokedCertifications mt verified)+ )+ checkUAtSigs+ :: [([UserAttrSubPacket], [SignaturePayload])]+ -> [([UserAttrSubPacket], [SignaturePayload])]+ checkUAtSigs =+ map+ ( \(uat, sps) ->+ let verified = rights . map (\sp -> fmap ((,) sp) (vUAt (uat, sp))) $ sps+ in (uat, retainNonRevokedCertifications mt verified)+ )+ checkSub+ :: (Pkt, [SignaturePayload]) -> [(Pkt, [SignaturePayload])]+ checkSub (pkt, sps) =+ if revokedSub pkt sps+ then []+ else checkSub' pkt sps+ revokedSub :: Pkt -> [SignaturePayload] -> Bool+ revokedSub _ [] = False+ revokedSub p sigs =+ any (vSubSig p) (filter subkeyRevocationEffective sigs)+ checkSub'+ :: Pkt -> [SignaturePayload] -> [(Pkt, [SignaturePayload])]+ checkSub' p sps =+ let goodsigs =+ filter (vSubSig p)+ . filter signatureKnown+ . filter isSubkeyBindingSig+ $ sps+ in if null goodsigs+ then []+ else [(p, goodsigs)]+ getHasheds = signatureHashedSubpackets+ filterRevs+ :: [(PubKeyAlgorithm, Fingerprint)]+ -> (SignaturePayload, Verification)+ -> [Either VerificationError SignaturePayload]+ filterRevs vokers spv =+ case spv of+ (s, _)+ | isV4OrV6Sig s && sigType s == Just SignatureDirectlyOnAKey ->+ [Right s | signatureKnown s]+ (s, v)+ | isV4OrV6Sig s+ , sigType s == Just KeyRevocationSig+ , Just pka <- sigPKA s ->+ if (v ^. verificationSigner == tk ^. tkuKey . _1)+ || any+ ( \(p, f) ->+ p == pka && f == fingerprint (v ^. verificationSigner)+ )+ vokers+ then+ if keyRevocationEffective s+ then [verificationError KeyRevoked]+ else [Right s | signatureKnown s]+ else [Right s | signatureKnown s]+ _ -> []+ isV4OrV6Sig = isVerifiableSignaturePayload+ vUid+ :: (Text, SignaturePayload) -> Either VerificationError Verification+ vUid (uid, sp) =+ vsf+ (SignaturePkt sp)+ emptyPSC+ { lastPrimaryKey = PublicKeyPkt (tk ^. tkuKey . _1)+ , lastUIDorUAt = UserIdPkt uid+ }+ Nothing+ vUAt+ :: ([UserAttrSubPacket], SignaturePayload)+ -> Either VerificationError Verification+ vUAt (uat, sp) =+ vsf+ (SignaturePkt sp)+ emptyPSC+ { lastPrimaryKey = PublicKeyPkt (tk ^. tkuKey . _1)+ , lastUIDorUAt = UserAttributePkt uat+ }+ Nothing+ vSig :: SignaturePayload -> Either VerificationError Verification+ vSig sp =+ vsf+ (SignaturePkt sp)+ emptyPSC {lastPrimaryKey = PublicKeyPkt (tk ^. tkuKey . _1)}+ Nothing+ vSubSig :: Pkt -> SignaturePayload -> Bool+ vSubSig sk sp =+ isRight+ ( vsf+ (SignaturePkt sp)+ emptyPSC+ { lastPrimaryKey = PublicKeyPkt (tk ^. tkuKey . _1)+ , lastSubkey = sk+ }+ mt+ )+ verifyRevoker+ :: SignaturePayload+ -> Either VerificationError [(PubKeyAlgorithm, Fingerprint)]+ verifyRevoker sp =+ vSig sp+ *> pure+ ( map (\(SigSubPacket _ (RevocationKey _ pka fp)) -> (pka, fp))+ . filter isRevocationKeySSP+ $ getHasheds sp+ )+ retainNonRevokedCertifications+ :: Maybe UTCTime+ -> [(SignaturePayload, Verification)]+ -> [SignaturePayload]+ retainNonRevokedCertifications validationTime verified =+ map fst $+ filter+ ( (== CertificationActive)+ . certificationStateAt validationTime verified+ )+ certifications+ where+ certifications = filter (not . isCertRevocationSig . fst) verified+ signatureKnown = signatureKnownAt mt+ subkeyRevocationEffective sp = isSubkeyRevocation sp && signatureEffectiveAt mt sp+ keyRevocationEffective sp+ | isHistoricalKeyRevocation sp = signatureEffectiveAt mt sp+ | otherwise = signatureUnexpiredAt mt sp++verifyAgainstKeyring+ :: PublicKeyring+ -> Pkt+ -> Maybe UTCTime+ -> ByteString+ -> Either VerificationError Verification+verifyAgainstKeyring kr sig mt payload = do+ let allKeys = map tkToUnknown (IxSet.toList kr)+ signerValidationTime = signatureCreationTimeFromPacket sig+ ikeys = (kr @=) <$> issuer sig+ ifpkeys = (kr @=) <$> issuerFP sig+ hintedKeys = maybe [] (map tkToUnknown . IxSet.toList) (ifpkeys <|> ikeys)+ hintedResult =+ if null hintedKeys+ then Left MissingIssuer+ else+ verifyFromCandidates+ allKeys+ hintedKeys+ sig+ signerValidationTime+ mt+ payload+ in case hintedResult of+ Right v -> Right v+ Left hintedErr ->+ let fallbackResult =+ verifyFromCandidates+ allKeys+ allKeys+ sig+ signerValidationTime+ mt+ payload+ in if null hintedKeys+ then case fallbackResult of+ Right v -> Right v+ Left _ ->+ verificationError+ (SigningKeyNotFound (issuer sig) (issuerFP sig))+ else case fallbackResult of+ Right v -> Right v+ Left _ -> Left hintedErr++verifyFromCandidates+ :: [TKUnknown]+ -> [TKUnknown]+ -> Pkt+ -> Maybe UTCTime+ -> Maybe UTCTime+ -> ByteString+ -> Either VerificationError Verification+verifyFromCandidates allKeys candidateTks sig signerValidationTime verificationTime payload =+ let candidateResults =+ map+ ( resolveCandidateSignerPKPs+ allKeys+ sig+ signerValidationTime+ (const True)+ )+ candidateTks+ candidateErrors = concatMap fst candidateResults+ usablePkps = concatMap snd candidateResults+ in if null usablePkps+ then+ if null candidateErrors+ then+ verificationError+ (SigningKeyNotFound (issuer sig) (issuerFP sig))+ else verificationError (CandidateKeyFailures candidateErrors)+ else case verifyAgainstPKPs usablePkps sig verificationTime payload of+ Left (CandidateKeyFailures errs)+ | not (null candidateErrors) ->+ verificationError+ (CandidateKeyFailures (candidateErrors ++ errs))+ other -> other++verifyAgainstKeys+ :: [TKUnknown]+ -> Pkt+ -> Maybe UTCTime+ -> ByteString+ -> Either VerificationError Verification+verifyAgainstKeys ks sig mt payload =+ verifyAgainstKeysWithPolicy+ defaultVerificationPolicy+ ks+ sig+ mt+ payload++-- | Verify a signature against a list of keys with a custom verification policy.+verifyAgainstKeysWithPolicy+ :: VerificationPolicy+ -> [TKUnknown]+ -> Pkt+ -> Maybe UTCTime+ -> ByteString+ -> Either VerificationError Verification+verifyAgainstKeysWithPolicy policy ks sig mt payload = do+ let allpkps =+ filter+ ( \x ->+ (((fingerprint x ==) <$> issuerFP sig) == Just True)+ || ((==) <$> issuer sig <*> hush (eightOctetKeyID x))+ == Just True+ )+ ( concatMap+ ( \x ->+ (x ^. tkuKey . _1)+ : mapMaybe (subkeyPKPFromPkt . fst) (_tkuSubs x)+ )+ ks+ )+ allCandidatePkps =+ concatMap+ ( \x ->+ (x ^. tkuKey . _1)+ : mapMaybe (subkeyPKPFromPkt . fst) (_tkuSubs x)+ )+ ks+ normalizedCandidates+ | null allpkps = allCandidatePkps+ | otherwise = allpkps+ verifyAgainstPKPsWithPolicy+ policy+ normalizedCandidates+ sig+ mt+ payload++verifyAgainstPKPs+ :: [SomePKPayload]+ -> Pkt+ -> Maybe UTCTime+ -> ByteString+ -> Either VerificationError Verification+verifyAgainstPKPs pkps sig mt payload =+ verifyAgainstPKPsWithPolicy+ defaultVerificationPolicy+ pkps+ sig+ mt+ payload++-- | Verify a signature against a list of public key payloads with a custom verification policy.+verifyAgainstPKPsWithPolicy+ :: VerificationPolicy+ -> [SomePKPayload]+ -> Pkt+ -> Maybe UTCTime+ -> ByteString+ -> Either VerificationError Verification+verifyAgainstPKPsWithPolicy policy pkps sig mt payload =+ case rights results of+ [] -> verificationError (CandidateKeyFailures (lefts results))+ [r] -> isSignatureExpired sig mt *> pure r+ rs -> verificationError (MultipleVerificationSuccesses (length rs))+ where+ results =+ map+ (\pkp -> verifyAgainstKeyWithPolicy policy pkp sig mt payload)+ pkps++resolveCandidateSignerPKPs+ :: [TKUnknown]+ -> Pkt+ -> Maybe UTCTime+ -> (SomePKPayload -> Bool)+ -> TKUnknown+ -> ([VerificationError], [SomePKPayload])+resolveCandidateSignerPKPs _ _ Nothing matchesP tk =+ ([], filter matchesP (candidatePKPs tk))+resolveCandidateSignerPKPs allKeys _ (Just validationTime) matchesP tk =+ let rawMatches = filter matchesP (candidatePKPs tk)+ in case verifyUnknownTKWith+ ( verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeys allKeys)+ )+ (Just validationTime)+ tk of+ Left err -> (replicate (length rawMatches) err, [])+ Right verifiedTK ->+ let verifiedMatches =+ map+ ( \pkp ->+ case historicallyValidSigner+ validationTime+ (timelineValidationTK pkp verifiedTK)+ pkp of+ Right () -> Right pkp+ Left err -> Left err+ )+ rawMatches+ in (lefts verifiedMatches, rights verifiedMatches)+ where+ timelineValidationTK pkp verifiedTK'+ | fingerprint pkp == fingerprint (tk ^. tkuKey . _1) = tk+ | otherwise = verifiedTK'++candidatePKPs :: TKUnknown -> [SomePKPayload]+candidatePKPs tk =+ (tk ^. tkuKey . _1)+ : mapMaybe (subkeyPKPFromPkt . fst) (tk ^. tkuSubs)++historicallyValidSigner+ :: UTCTime+ -> TKUnknown+ -> SomePKPayload+ -> Either VerificationError ()+historicallyValidSigner validationTime tk pkp+ | fingerprint pkp == fingerprint (tk ^. tkuKey . _1) =+ if keyStateValid (keyStateAt validationTime tk)+ then Right ()+ else verificationError SigningKeyUnavailableAtSignatureTime+ | otherwise =+ case find+ ( \(pkt, _) ->+ maybe+ False+ ((== fingerprint pkp) . fingerprint)+ (subkeyPKPFromPkt pkt)+ )+ (tk ^. tkuSubs) of+ Nothing -> verificationError SigningKeyUnavailableAtSignatureTime+ Just (subPkt, sigs) ->+ case subkeyPKPFromPkt subPkt of+ Nothing -> verificationError SigningKeyUnavailableAtSignatureTime+ Just subPKP ->+ if isPKTimeValidWithSelfSignatures validationTime subPKP sigs+ then Right ()+ else verificationError SigningKeyUnavailableAtSignatureTime++signatureCreationTimeFromPacket :: Pkt -> Maybe UTCTime+signatureCreationTimeFromPacket (SignaturePkt sigPayload) = signatureCreationTime sigPayload+signatureCreationTimeFromPacket _ = Nothing++signatureCreationTime :: SignaturePayload -> Maybe UTCTime+signatureCreationTime =+ fmap+ (posixSecondsToUTCTime . realToFrac . unThirtyTwoBitTimeStamp)+ . sigCT++signatureKnownAt :: Maybe UTCTime -> SignaturePayload -> Bool+signatureKnownAt Nothing _ = True+signatureKnownAt (Just validationTime) sigPayload =+ maybe+ False+ (<= validationTime)+ (signatureCreationTime sigPayload)++signatureEffectiveAt :: Maybe UTCTime -> SignaturePayload -> Bool+signatureEffectiveAt Nothing _ = True+signatureEffectiveAt (Just validationTime) sigPayload =+ signatureKnownAt (Just validationTime) sigPayload+ && maybe+ True+ (validationTime <)+ (signatureExpirationTime sigPayload)++signatureUnexpiredAt :: Maybe UTCTime -> SignaturePayload -> Bool+signatureUnexpiredAt Nothing _ = True+signatureUnexpiredAt (Just validationTime) sigPayload =+ maybe+ True+ (validationTime <)+ (signatureExpirationTime sigPayload)++signatureExpirationTime :: SignaturePayload -> Maybe UTCTime+signatureExpirationTime sigPayload =+ addDurationToTime+ <$> signatureCreationTime sigPayload+ <*> signatureExpirationDuration sigPayload++signatureExpirationDuration+ :: SignaturePayload -> Maybe ThirtyTwoBitDuration+signatureExpirationDuration sigPayload =+ signatureHashedSubpacketsKnown sigPayload+ >>= firstSignatureExpirationDuration++firstSignatureExpirationDuration+ :: [SigSubPacket] -> Maybe ThirtyTwoBitDuration+firstSignatureExpirationDuration =+ foldr+ ( \subpacket acc ->+ case subpacket of+ SigSubPacket _ (SigExpirationTime duration) -> Just duration+ _ -> acc+ )+ Nothing++addDurationToTime :: UTCTime -> ThirtyTwoBitDuration -> UTCTime+addDurationToTime baseTime duration =+ addUTCTime+ (fromIntegral (unThirtyTwoBitDuration duration))+ baseTime++certificationStateAt+ :: Maybe UTCTime+ -> [(SignaturePayload, Verification)]+ -> (SignaturePayload, Verification)+ -> CertificationState+certificationStateAt validationTime verified certification@(certificationSig, _)+ | not (signatureKnownAt validationTime certificationSig) =+ CertificationNotYetKnown+ | not (signatureEffectiveAt validationTime certificationSig) =+ CertificationRevoked+ | any (`revokesCertification` certification) visibleRevocations =+ CertificationRevoked+ | otherwise = CertificationActive+ where+ visibleRevocations =+ filter+ (signatureEffectiveAt validationTime . fst)+ (filter (isCertRevocationSig . fst) verified)++revokesCertification+ :: (SignaturePayload, Verification)+ -> (SignaturePayload, Verification)+ -> Bool+revokesCertification (revocationSig, revocationVerification) (certificationSig, certificationVerification) =+ sameSigner+ && certificationPrecedesRevocation certificationSig revocationSig+ where+ sameSigner =+ fingerprint (revocationVerification ^. verificationSigner)+ == fingerprint (certificationVerification ^. verificationSigner)++certificationPrecedesRevocation+ :: SignaturePayload -> SignaturePayload -> Bool+certificationPrecedesRevocation certificationSig revocationSig =+ case ( signatureCreationTime certificationSig+ , signatureCreationTime revocationSig+ ) of+ (Just certificationTime, Just revocationTime) ->+ certificationTime < revocationTime+ _ -> False++isHistoricalKeyRevocation :: SignaturePayload -> Bool+isHistoricalKeyRevocation sigPayload =+ case revocationReasonCode sigPayload of+ Just KeySuperseded -> True+ Just KeyRetiredAndNoLongerUsed -> True+ Just UserIdInfoNoLongerValid -> True+ _ -> False++revocationReasonCode :: SignaturePayload -> Maybe RevocationCode+revocationReasonCode sigPayload =+ ( \(SigSubPacket _ (ReasonForRevocation reasonCode _)) -> reasonCode+ )+ <$> find isReasonForRevocation (signatureSubpackets sigPayload)+ where+ isReasonForRevocation (SigSubPacket _ ReasonForRevocation {}) = True+ isReasonForRevocation _ = False++signatureSubpackets :: SignaturePayload -> [SigSubPacket]+signatureSubpackets sigPayload =+ case signatureSubpacketListsKnown sigPayload of+ Just (hashed, unhashed) -> hashed ++ unhashed+ Nothing -> []++signatureHashedSubpackets :: SignaturePayload -> [SigSubPacket]+signatureHashedSubpackets sigPayload =+ maybe [] id (signatureHashedSubpacketsKnown sigPayload)++subkeyPKPFromPkt :: Pkt -> Maybe SomePKPayload+subkeyPKPFromPkt (PublicSubkeyPkt p) = Just p+subkeyPKPFromPkt (SecretSubkeyPkt p _) = Just p+subkeyPKPFromPkt _ = Nothing++verifyAgainstKey'+ :: SomePKPayload+ -> Pkt+ -> Maybe UTCTime+ -> ByteString+ -> Either VerificationError Verification+verifyAgainstKey' pkp sig mt payload =+ verifyAgainstKeyWithPolicy+ defaultVerificationPolicy+ pkp+ sig+ mt+ payload++{- | Verify a signature against a key with a custom verification policy.+This allows callers to control whether certain signature features+(deprecated hash algorithms, PKA mismatches, etc.) are treated as+hard errors or warnings.+-}+verifyAgainstKeyWithPolicy+ :: VerificationPolicy+ -> SomePKPayload+ -> Pkt+ -> Maybe UTCTime+ -> ByteString+ -> Either VerificationError Verification+verifyAgainstKeyWithPolicy policy pkp sig mt payload = do+ sigClass <-+ either+ (verificationError . const NonSignaturePacket)+ Right+ (fromPktEitherSomeSignatureV sig)+ let sigPayload =+ case sigClass of+ SomeSignatureV typedSig -> signaturePayloadFromSignatureV typedSig+ sigHash <-+ maybe+ (verificationError MissingHashAlgorithm)+ Right+ (sigHA sigPayload)+ sigDetails <- signaturePKAAndMPIsFromClass sigClass+ warnings <- enforcePKACompatibility policy sigPayload+ hashWarnings <- enforceSignatureHashPolicy policy sigHash+ _ <- isSignatureExpired sig mt+ let signedPayload = BL.toStrict (finalPayload sig payload)+ enforceLeft16Prefix sigClass sigHash signedPayload+ ( \verifiedSigner ->+ Verification verifiedSigner sigPayload (warnings ++ hashWarnings)+ )+ <$> verify' sigDetails pkp sigHash signedPayload+ where+ enforcePKACompatibility vp sigPayload =+ let sigPka = maybe (OtherPKA 0) id (sigPKA sigPayload)+ keyPka = _pkalgo pkp+ in if pkaCompatible sigPka keyPka+ then Right []+ else case applyVerificationPolicy+ (vpPkaMismatch vp)+ ( "PKA mismatch: signature uses "+ ++ show sigPka+ ++ " but key uses "+ ++ show keyPka+ ) of+ Left err -> verificationError (SignaturePolicyPKAMismatch sigPka keyPka)+ Right warn -> Right [PkaMismatchWarning sigPka keyPka]+ enforceSignatureHashPolicy vp sigHash =+ case sigHash of+ OtherHA {} ->+ case applyVerificationPolicy+ (vpUnsupportedHashAlgorithm vp)+ ("Unsupported hash algorithm: " ++ show sigHash) of+ Left err -> verificationError (SignaturePolicyHashUnsupported sigHash)+ Right warn -> Right [UnsupportedHashAlgorithmWarning sigHash]+ DeprecatedMD5 ->+ case applyVerificationPolicy+ (vpDeprecatedHashAlgorithm vp)+ ("Deprecated hash algorithm: MD5") of+ Left err -> verificationError (SignaturePolicyHashUnsupported sigHash)+ Right warn -> Right [DeprecatedHashAlgorithmWarning sigHash]+ SHA1 ->+ case applyVerificationPolicy+ (vpDeprecatedHashAlgorithm vp)+ ("Deprecated hash algorithm: SHA1") of+ Left err -> verificationError (SignaturePolicyHashUnsupported sigHash)+ Right warn -> Right [DeprecatedHashAlgorithmWarning sigHash]+ RIPEMD160 ->+ case applyVerificationPolicy+ (vpDeprecatedHashAlgorithm vp)+ ("Deprecated hash algorithm: RIPEMD160") of+ Left err -> verificationError (SignaturePolicyHashUnsupported sigHash)+ Right warn -> Right [DeprecatedHashAlgorithmWarning sigHash]+ _ -> Right []+ enforceLeft16Prefix sigClass sigHash signedPayload = do+ expectedLeft16 <-+ either+ (verificationError . HashComputationFailed)+ Right+ (left16FromSignedPayload sigHash signedPayload)+ actualLeft16 <- signatureLeft16FromClass sigClass+ if actualLeft16 == expectedLeft16+ then Right ()+ else+ verificationError+ (SignatureMismatch (_pkalgo pkp) (fingerprint pkp))+ pkaCompatible RSA keyPka =+ keyPka+ `elem` [RSA, DeprecatedRSAEncryptOnly, DeprecatedRSASignOnly]+ pkaCompatible DeprecatedRSASignOnly keyPka =+ keyPka `elem` [RSA, DeprecatedRSASignOnly]+ pkaCompatible PKA.EdDSA keyPka =+ keyPka `elem` [PKA.EdDSA, PKA.Ed25519, PKA.Ed448]+ pkaCompatible PKA.Ed25519 keyPka =+ keyPka `elem` [PKA.EdDSA, PKA.Ed25519]+ pkaCompatible PKA.Ed448 keyPka =+ keyPka `elem` [PKA.EdDSA, PKA.Ed448]+ pkaCompatible sigPka keyPka = sigPka == keyPka+ verify' details pub@(PKPayload V4 _ _ _ pkey) ha pl =+ verifyByHash details pub pkey ha pl+ verify' details pub@(PKPayload V6 _ _ _ pkey) ha pl =+ verifyByHash details pub pkey ha pl+ verify' _ _ _ _ =+ verificationError UnexpectedKeyVersion+ verifyByHash details pub pkey ha pl =+ case ha of+ SHA1 -> verify'' details CHA.SHA1 pub pkey pl+ RIPEMD160 -> verify'' details CHA.RIPEMD160 pub pkey pl+ SHA224 -> verify'' details CHA.SHA224 pub pkey pl+ SHA256 -> verify'' details CHA.SHA256 pub pkey pl+ SHA384 -> verify'' details CHA.SHA384 pub pkey pl+ SHA512 -> verify'' details CHA.SHA512 pub pkey pl+ SHA3_256 -> verifyNoRSA SHA3_256 details CHA.SHA3_256 pub pkey pl+ SHA3_512 -> verifyNoRSA SHA3_512 details CHA.SHA3_512 pub pkey pl+ DeprecatedMD5 -> verify'' details CHA.MD5 pub pkey pl+ _ ->+ verificationError (SignaturePolicyHashUnsupported ha)+ verify'' (DSA, mpis) hd pub (DSAPubKey (DSA_PublicKey pkey)) bs =+ dsaVerify pub mpis hd pkey bs+ verify'' (ECDSA, mpis) hd pub (ECDSAPubKey (ECDSA_PublicKey pkey)) bs =+ ecdsaVerify pub mpis hd pkey bs+ verify'' (sigPka, mpis) hd pub (EdDSAPubKey EdSigningCurve25519 pkey) bs+ | sigPka `elem` [EdDSA, PKA.Ed25519] =+ ed25519Verify sigPka pub mpis hd pkey bs+ verify'' (sigPka, mpis) hd pub (EdDSAPubKey EdSigningCurve448 pkey) bs+ | sigPka `elem` [EdDSA, PKA.Ed448] =+ ed448Verify sigPka pub mpis hd pkey bs+ verify'' (RSA, mpis) hd pub (RSAPubKey (RSA_PublicKey pkey)) bs =+ rsaVerify pub mpis hd pkey bs+ verify'' (pka, _) _ _ _ _ = verificationError (UnsupportedKeyType pka)+ verifyNoRSA ha' (DSA, mpis) hd pub (DSAPubKey (DSA_PublicKey pkey)) bs =+ dsaVerify pub mpis hd pkey bs+ verifyNoRSA ha' (ECDSA, mpis) hd pub (ECDSAPubKey (ECDSA_PublicKey pkey)) bs =+ ecdsaVerify pub mpis hd pkey bs+ verifyNoRSA ha' (sigPka, mpis) hd pub (EdDSAPubKey EdSigningCurve25519 pkey) bs+ | sigPka `elem` [EdDSA, PKA.Ed25519] =+ ed25519Verify sigPka pub mpis hd pkey bs+ verifyNoRSA ha' (sigPka, mpis) hd pub (EdDSAPubKey EdSigningCurve448 pkey) bs+ | sigPka `elem` [EdDSA, PKA.Ed448] =+ ed448Verify sigPka pub mpis hd pkey bs+ verifyNoRSA ha' (RSA, _) _ _ _ _ =+ verificationError (SignatureHashUnsupportedByAlgorithm ha' RSA)+ verifyNoRSA _ (pka, _) _ _ _ _ = verificationError (UnsupportedKeyType pka)+ dsaVerify pub (r :| [s]) hd pkey bs =+ if DSA.verify hd pkey (dsaMPIsToSig r s) bs+ then Right pub+ else verificationError (SignatureMismatch DSA (fingerprint pub))+ dsaVerify _ _ _ _ _ = verificationError (SignatureShapeMismatch DSA)+ ecdsaVerify pub (r :| [s]) hd pkey bs =+ if ECDSA.verify hd pkey (ecdsaMPIsToSig r s) bs+ then Right pub+ else+ verificationError (SignatureMismatch ECDSA (fingerprint pub))+ ecdsaVerify _ _ _ _ _ = verificationError (SignatureShapeMismatch ECDSA)+ ed25519Verify sigPka pub (r :| [s]) hd pkey bs =+ case edPointToRawPublic 32 pkey of+ Left err ->+ verificationError (SignatureEncodingInvalid sigPka err)+ Right rawPub ->+ case cf2es (Ed25519.publicKey rawPub) of+ Left err ->+ verificationError (SignatureEncodingInvalid sigPka err)+ Right ep ->+ case cf2es+ ( Ed25519.signature+ (pad32 (i2osp (unMPI r)) <> pad32 (i2osp (unMPI s)))+ ) of+ Left err ->+ verificationError (SignatureEncodingInvalid sigPka err)+ Right es ->+ let prehash = crazyHash hd bs :: B.ByteString+ in if Ed25519.verify ep prehash es+ then Right pub+ else+ verificationError (SignatureMismatch sigPka (fingerprint pub))+ ed25519Verify sigPka _ _ _ _ _ =+ verificationError (SignatureShapeMismatch sigPka)+ ed448Verify sigPka pub (r :| [s]) hd pkey bs =+ case edPointToRawPublic 57 pkey of+ Left err ->+ verificationError (SignatureEncodingInvalid sigPka err)+ Right rawPub ->+ case cf2es (Ed448.publicKey rawPub) of+ Left err ->+ verificationError (SignatureEncodingInvalid sigPka err)+ Right ep ->+ case cf2es+ ( Ed448.signature+ (padN 57 (i2osp (unMPI r)) <> padN 57 (i2osp (unMPI s)))+ ) of+ Left err ->+ verificationError (SignatureEncodingInvalid sigPka err)+ Right es ->+ let prehash = crazyHash hd bs :: B.ByteString+ in if Ed448.verify ep prehash es+ then Right pub+ else+ verificationError (SignatureMismatch sigPka (fingerprint pub))+ ed448Verify sigPka _ _ _ _ _ =+ verificationError (SignatureShapeMismatch sigPka)+ edPointToRawPublic expectedLen (NativeEPoint (EPoint x)) =+ exactLengthPublic expectedLen "native" (i2osp x)+ edPointToRawPublic expectedLen (PrefixedNativeEPoint (EPoint x)) = do+ prefixed <-+ exactLengthPublic (expectedLen + 1) "prefixed-native" (i2osp x)+ if B.head prefixed /= 0x40+ then+ Left+ "prefixed-native EdDSA public key is missing the 0x40 prefix"+ else Right (B.tail prefixed)+ exactLengthPublic expectedLen label bs+ | B.length bs == expectedLen = Right bs+ | otherwise =+ Left+ ( "invalid "+ ++ label+ ++ " EdDSA public key length: expected "+ ++ show expectedLen+ ++ " octets, got "+ ++ show (B.length bs)+ )+ pad32 bs =+ let l = B.length bs+ in if l >= 32+ then bs+ else B.replicate (32 - l) 0 <> bs+ padN n bs =+ let l = B.length bs+ in if l >= n+ then bs+ else B.replicate (n - l) 0 <> bs+ cf2es = either (Left . show) return . 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))+ 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)+ pad = sz - B.length raw+ in B.replicate pad 0 <> raw+ crazyHash h = BA.convert . hashWith h++isSignatureExpired+ :: Pkt -> Maybe UTCTime -> Either VerificationError Bool+isSignatureExpired _ Nothing = return False+isSignatureExpired s (Just t) =+ do+ sigClass <-+ either+ (verificationError . const NonSignaturePacket)+ Right+ (fromPktEitherSomeSignatureV s)+ if any+ (expiredBefore t)+ ( signatureHashedSubpackets+ ( case sigClass of+ SomeSignatureV typedSig -> signaturePayloadFromSignatureV typedSig+ )+ )+ then verificationError SignatureExpired+ else return True+ where+ expiredBefore :: UTCTime -> SigSubPacket -> Bool+ expiredBefore ct (SigSubPacket _ (SigExpirationTime et)) =+ fromEnum+ ((posixSecondsToUTCTime . toEnum . fromEnum) et `diffUTCTime` ct)+ < 0+ expiredBefore _ _ = False++finalPayload :: Pkt -> ByteString -> ByteString+finalPayload s pl = BL.concat [pl, sigbit, trailer s]+ where+ sigbit = runPut $ putPartialSigforSigning s+ trailer :: Pkt -> ByteString+ trailer (SignaturePkt sigPayload) =+ maybe+ BL.empty+ (const (runPut $ putSigTrailer s))+ (fromSignaturePayloadVerifiableSignatureV sigPayload)+ trailer _ = BL.empty++normalizePayloadForSigType :: SigType -> ByteString -> ByteString+normalizePayloadForSigType CanonicalTextSig =+ stripTrailingWhitespacePerLine . canonicalizeLineEndings+normalizePayloadForSigType _ = id++normalizePayloadForSigTypeWith+ :: TextNormalizationMode -> SigType -> ByteString -> ByteString+normalizePayloadForSigTypeWith CleartextCompat st = normalizePayloadForSigType st+normalizePayloadForSigTypeWith RFC9580Strict CanonicalTextSig = canonicalizeLineEndings+normalizePayloadForSigTypeWith RFC9580Strict _ = id++canonicalizeLineEndings :: ByteString -> ByteString+canonicalizeLineEndings = BL.pack . go . BL.unpack+ where+ go [] = []+ go (0x0d : 0x0a : rest) = 0x0d : 0x0a : go rest+ go (0x0d : rest) = 0x0d : 0x0a : go rest+ go (0x0a : rest) = 0x0d : 0x0a : go rest+ go (w : rest) = w : go rest++stripTrailingWhitespacePerLine :: ByteString -> ByteString+stripTrailingWhitespacePerLine = BL.pack . go [] . BL.unpack+ where+ go lineRev [] = reverseTrimmed lineRev+ go lineRev (0x0d : 0x0a : rest) =+ reverseTrimmed lineRev ++ [0x0d, 0x0a] ++ go [] rest+ go lineRev (w : rest) = go (w : lineRev) rest++ reverseTrimmed :: [Word8] -> [Word8]+ reverseTrimmed = reverse . dropWhile isTrailingWhitespace++ isTrailingWhitespace :: Word8 -> Bool+ isTrailingWhitespace w = w == 0x20 || w == 0x09++hashWithSHA512 :: B.ByteString -> B.ByteString+hashWithSHA512 = BA.convert . hashWith CHA.SHA512++hashForSignatureAlgorithm+ :: HashAlgorithm -> B.ByteString -> Either String B.ByteString+hashForSignatureAlgorithm ha bs =+ case ha of+ SHA1 -> Right (BA.convert (hashWith CHA.SHA1 bs))+ RIPEMD160 -> Right (BA.convert (hashWith CHA.RIPEMD160 bs))+ SHA224 -> Right (BA.convert (hashWith CHA.SHA224 bs))+ SHA256 -> Right (BA.convert (hashWith CHA.SHA256 bs))+ SHA384 -> Right (BA.convert (hashWith CHA.SHA384 bs))+ SHA512 -> Right (BA.convert (hashWith CHA.SHA512 bs))+ SHA3_256 -> Right (BA.convert (hashWith CHA.SHA3_256 bs))+ SHA3_512 -> Right (BA.convert (hashWith CHA.SHA3_512 bs))+ DeprecatedMD5 -> Right (BA.convert (hashWith CHA.MD5 bs))+ _ ->+ Left+ ("unsupported hash algorithm for left16 derivation: " ++ show ha)++left16FromHashPrefix :: B.ByteString -> Either String Word16+left16FromHashPrefix bs+ | B.length bs >= 2 = Right (fromIntegral (os2ip (B.take 2 bs)))+ | otherwise = Left "hash output too short to derive left16"++left16FromSignedPayload+ :: HashAlgorithm -> B.ByteString -> Either String Word16+left16FromSignedPayload ha signedPayload = do+ digest <- hashForSignatureAlgorithm ha signedPayload+ left16FromHashPrefix digest++left16FromSignedPayloadForSign+ :: HashAlgorithm -> B.ByteString -> Either SignError Word16+left16FromSignedPayloadForSign ha =+ first SignBackendError . left16FromSignedPayload ha++ed25519Signer+ :: Ed25519.SecretKey -> B.ByteString -> B.ByteString+ed25519Signer sk prehash =+ BA.convert (Ed25519.sign sk (Ed25519.toPublic sk) prehash)++ed448Signer :: Ed448.SecretKey -> B.ByteString -> B.ByteString+ed448Signer sk prehash =+ BA.convert (Ed448.sign sk (Ed448.toPublic sk) prehash)++rsaPKCS15Sign+ :: HashAlgorithm+ -> RSATypes.PrivateKey+ -> B.ByteString+ -> Either SignError B.ByteString+rsaPKCS15Sign ha prv bytes =+ case ha of+ SHA1 ->+ first+ (SignBackendError . show)+ (P15.sign Nothing (Just CHA.SHA1) prv bytes)+ SHA224 ->+ first+ (SignBackendError . show)+ (P15.sign Nothing (Just CHA.SHA224) prv bytes)+ SHA256 ->+ first+ (SignBackendError . show)+ (P15.sign Nothing (Just CHA.SHA256) prv bytes)+ SHA384 ->+ first+ (SignBackendError . show)+ (P15.sign Nothing (Just CHA.SHA384) prv bytes)+ SHA512 ->+ first+ (SignBackendError . show)+ (P15.sign Nothing (Just CHA.SHA512) prv bytes)+ _ ->+ Left+ ( SignBackendError+ ( "signature hash algorithm is not supported by RSA PKCS#1 v1.5 backend: "+ ++ show ha+ )+ )++validateV6SaltSize+ :: HashAlgorithm -> SignatureSalt -> Either SignError ()+validateV6SaltSize ha salt =+ let saltBytes = BL.toStrict (unSignatureSalt salt)+ actualSaltLen = B.length saltBytes+ in case signatureV6SaltSizeForHashAlgorithm ha of+ Nothing ->+ Left+ ( SignBackendError+ ( "signature hash algorithm does not define a V6 salt size: "+ ++ show ha+ )+ )+ Just expectedSaltLen ->+ if actualSaltLen == fromIntegral expectedSaltLen+ then Right ()+ else+ Left (SignV6SaltSizeMismatch ha expectedSaltLen actualSaltLen)++signEdDSAV4+ :: String+ -> Int+ -> Int+ -> (B.ByteString -> B.ByteString)+ -> TextNormalizationMode+ -> SigType+ -> PubKeyAlgorithm+ -> HashAlgorithm+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> ByteString+ -> Either SignError SignaturePayload+signEdDSAV4 algoName sigLen limbLen signer mode st pka ha has uhas payload = do+ let normalizedPayload = normalizePayloadForSigTypeWith mode st payload+ sig0 = SigV4 st pka ha has [] 0 (NE.fromList [MPI 0, MPI 0])+ prehash =+ hashWithSHA512+ (BL.toStrict (finalPayload (SignaturePkt sig0) normalizedPayload))+ sigBytes = signer prehash+ if B.length sigBytes /= sigLen+ then+ Left+ (SignProducedWrongLength algoName sigLen (B.length sigBytes))+ else+ let (r, s) = B.splitAt limbLen sigBytes+ in ( \left16 ->+ SigV4+ st+ pka+ ha+ has+ uhas+ left16+ (NE.fromList [MPI (os2ip r), MPI (os2ip s)])+ )+ <$> first SignBackendError (left16FromHashPrefix prehash)++signEdDSAV6+ :: String+ -> Int+ -> Int+ -> (B.ByteString -> B.ByteString)+ -> TextNormalizationMode+ -> SigType+ -> PubKeyAlgorithm+ -> HashAlgorithm+ -> SignatureSalt+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> ByteString+ -> Either SignError SignaturePayload+signEdDSAV6 algoName sigLen limbLen signer mode st pka ha salt has uhas payload = do+ validateV6SaltSize ha salt+ let normalizedPayload = normalizePayloadForSigTypeWith mode st payload+ sig0 = SigV6 st pka ha salt has [] 0 (NE.fromList [MPI 0, MPI 0])+ prehash =+ hashWithSHA512+ (BL.toStrict (finalPayload (SignaturePkt sig0) normalizedPayload))+ sigBytes = signer prehash+ if B.length sigBytes /= sigLen+ then+ Left+ (SignProducedWrongLength algoName sigLen (B.length sigBytes))+ else+ let (r, s) = B.splitAt limbLen sigBytes+ in ( \left16 ->+ SigV6+ st+ pka+ ha+ salt+ has+ uhas+ left16+ (NE.fromList [MPI (os2ip r), MPI (os2ip s)])+ )+ <$> first SignBackendError (left16FromHashPrefix prehash)++signUserIDwithRSA+ :: SomePKPayload+ -- ^ public key "payload" of user ID being signed+ -> UserId+ -- ^ user ID being signed+ -> [SigSubPacket]+ -- ^ hashed signature subpackets+ -> [SigSubPacket]+ -- ^ unhashed signature subpackets+ -> RSATypes.PrivateKey+ -- ^ RSA signing key+ -> Either SignError SignaturePayload+signUserIDwithRSA = signCertificationWithRSA PositiveCert++signCertificationWithRSA+ :: SigType+ -- ^ certification type (GenericCert, PersonaCert, CasualCert, PositiveCert)+ -> SomePKPayload+ -- ^ public key "payload" of user ID being signed+ -> UserId+ -- ^ user ID being signed+ -> [SigSubPacket]+ -- ^ hashed signature subpackets+ -> [SigSubPacket]+ -- ^ unhashed signature subpackets+ -> RSATypes.PrivateKey+ -- ^ RSA signing key+ -> Either SignError SignaturePayload+signCertificationWithRSA st pkp uid hsigsubs usigsubs prv+ | st `elem` [GenericCert, PersonaCert, CasualCert, PositiveCert] = do+ let payloadToSign = BL.toStrict (finalPayload (SignaturePkt uidsigp) uidpayload)+ uidsigp'+ <$> left16FromSignedPayloadForSign SHA512 payloadToSign+ <*> first+ (SignBackendError . show)+ ( P15.sign+ Nothing+ (Just CHA.SHA512)+ prv+ payloadToSign+ )+ | otherwise =+ Left (SignUnsupportedCertificationType st)+ where+ uidpayload =+ runPut+ ( sequence_+ [putKeyforSigning (PublicKeyPkt pkp), putUforSigning (toPkt uid)]+ )+ uidsigp =+ SigV4 st RSA SHA512 hsigsubs usigsubs 0 (NE.fromList [MPI 0])+ uidsigp' left16 us =+ SigV4+ st+ RSA+ SHA512+ hsigsubs+ usigsubs+ left16+ (NE.fromList [MPI (os2ip us)])++signDirectKeyWithRSA+ :: SigType+ -- ^ key-scoped signature type (SignatureDirectlyOnAKey or KeyRevocationSig)+ -> SomePKPayload+ -- ^ primary key "payload" being signed+ -> [SigSubPacket]+ -- ^ hashed signature subpackets+ -> [SigSubPacket]+ -- ^ unhashed signature subpackets+ -> RSATypes.PrivateKey+ -- ^ RSA signing key+ -> Either SignError SignaturePayload+signDirectKeyWithRSA st pkp hsigsubs usigsubs prv+ | st `elem` [SignatureDirectlyOnAKey, KeyRevocationSig] =+ signDataWithRSA st prv hsigsubs usigsubs keypayload+ | otherwise =+ Left (SignUnsupportedKeySignatureType st)+ where+ keypayload = runPut (putKeyforSigning (PublicKeyPkt pkp))++signKeyRevocationWithRSA+ :: SomePKPayload+ -- ^ primary key "payload" being revoked+ -> [SigSubPacket]+ -- ^ hashed signature subpackets+ -> [SigSubPacket]+ -- ^ unhashed signature subpackets+ -> RSATypes.PrivateKey+ -- ^ RSA signing key+ -> Either SignError SignaturePayload+signKeyRevocationWithRSA = signDirectKeyWithRSA KeyRevocationSig++signSubkeyRevocationWithRSA+ :: SomePKPayload+ -- ^ primary key "payload"+ -> SomePKPayload+ -- ^ public subkey "payload" being revoked+ -> [SigSubPacket]+ -- ^ hashed signature subpackets+ -> [SigSubPacket]+ -- ^ unhashed signature subpackets+ -> RSATypes.PrivateKey+ -- ^ RSA signing key+ -> Either SignError SignaturePayload+signSubkeyRevocationWithRSA pkp subpkp hsigsubs usigsubs prv =+ signDataWithRSA+ SubkeyRevocationSig+ prv+ hsigsubs+ usigsubs+ subkeypayload+ where+ subkeypayload =+ runPut+ ( sequence_+ [ putKeyforSigning (PublicKeyPkt pkp)+ , putKeyforSigning (PublicSubkeyPkt subpkp)+ ]+ )++signCertRevocationWithRSA+ :: SomePKPayload+ -- ^ primary key "payload"+ -> UserId+ -- ^ user ID certification being revoked+ -> [SigSubPacket]+ -- ^ hashed signature subpackets+ -> [SigSubPacket]+ -- ^ unhashed signature subpackets+ -> RSATypes.PrivateKey+ -- ^ RSA signing key+ -> Either SignError SignaturePayload+signCertRevocationWithRSA pkp uid hsigsubs usigsubs prv =+ signDataWithRSA+ CertRevocationSig+ prv+ hsigsubs+ usigsubs+ certpayload+ where+ certpayload =+ runPut+ ( sequence_+ [putKeyforSigning (PublicKeyPkt pkp), putUforSigning (toPkt uid)]+ )++crossSignSubkeyWithRSA+ :: SomePKPayload+ -- ^ public key "payload" of key being signed+ -> SomePKPayload+ -- ^ public subkey "payload" of key being signed+ -> [SigSubPacket]+ -- ^ hashed signature subpackets for binding sig+ -> [SigSubPacket]+ -- ^ unhashed signature subpackets for binding sig+ -> [SigSubPacket]+ -- ^ hashed signature subpackets for embedded sig+ -> [SigSubPacket]+ -- ^ unhashed signature subpackets for embedded sig+ -> RSATypes.PrivateKey+ -- ^ RSA signing key+ -> RSATypes.PrivateKey+ -- ^ RSA signing subkey+ -> Either SignError SignaturePayload+crossSignSubkeyWithRSA pkp subpkp subhsigsubs subusigsubs embhsigsubs embusigsubs prv ssb = do+ let embPayloadToSign =+ BL.toStrict (finalPayload (SignaturePkt embsigp) subkeypayload)+ subPayloadToSign =+ BL.toStrict (finalPayload (SignaturePkt subsigp) subkeypayload)+ ( \embleft16 subleft16 embsig subsig ->+ subsigp' (embsigp' embleft16 embsig) subleft16 subsig+ )+ <$> left16FromSignedPayloadForSign SHA512 embPayloadToSign+ <*> left16FromSignedPayloadForSign SHA512 subPayloadToSign+ <*> first+ (SignBackendError . show)+ ( P15.sign+ Nothing+ (Just CHA.SHA512)+ ssb+ embPayloadToSign+ )+ <*> first+ (SignBackendError . show)+ ( P15.sign+ Nothing+ (Just CHA.SHA512)+ prv+ subPayloadToSign+ )+ where+ subkeypayload =+ runPut+ ( sequence_+ [ putKeyforSigning (PublicKeyPkt pkp)+ , putKeyforSigning (PublicSubkeyPkt subpkp)+ ]+ )+ embsigp =+ SigV4+ PrimaryKeyBindingSig+ RSA+ SHA512+ embhsigsubs+ embusigsubs+ 0+ (NE.fromList [MPI 0])+ embsigp' left16 es =+ SigV4+ PrimaryKeyBindingSig+ RSA+ SHA512+ embhsigsubs+ embusigsubs+ left16+ (NE.fromList [MPI (os2ip es)])+ subsigp =+ SigV4+ SubkeyBindingSig+ RSA+ SHA512+ subhsigsubs+ []+ 0+ (NE.fromList [MPI 0])+ sspes es = SigSubPacket False (EmbeddedSignature es)+ subsigp' es left16 ss =+ SigV4+ SubkeyBindingSig+ RSA+ SHA512+ subhsigsubs+ (sspes es : subusigsubs)+ left16+ (NE.fromList [MPI (os2ip ss)])++signRSAV4Core+ :: TextNormalizationMode+ -> SigType+ -> HashAlgorithm+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> RSATypes.PrivateKey+ -> ByteString+ -> Either SignError SignaturePayload+signRSAV4Core mode st ha has uhas prv payload =+ ( \left16 ss ->+ SigV4 st RSA ha has uhas left16 (NE.fromList [MPI (os2ip ss)])+ )+ <$> left16FromSignedPayloadForSign ha payloadToSign+ <*> rsaPKCS15Sign ha prv payloadToSign+ where+ sig0 = SigV4 st RSA ha has [] 0 (NE.fromList [MPI 0])+ payloadToSign =+ BL.toStrict+ ( finalPayload+ (SignaturePkt sig0)+ (normalizePayloadForSigTypeWith mode st payload)+ )++signRSAV6Core+ :: TextNormalizationMode+ -> SigType+ -> HashAlgorithm+ -> SignatureSalt+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> RSATypes.PrivateKey+ -> ByteString+ -> Either SignError SignaturePayload+signRSAV6Core mode st ha salt has uhas prv payload = do+ validateV6SaltSize ha salt+ ( \left16 sigBytes ->+ SigV6+ st+ RSA+ ha+ salt+ has+ uhas+ left16+ (NE.fromList [MPI (os2ip sigBytes)])+ )+ <$> left16FromSignedPayloadForSign ha payloadToSign+ <*> rsaPKCS15Sign ha prv payloadToSign+ where+ sig0 = SigV6 st RSA ha salt has [] 0 (NE.fromList [MPI 0])+ payloadToSign =+ BL.toStrict+ ( finalPayload+ (SignaturePkt sig0)+ (normalizePayloadForSigTypeWith mode st payload)+ )++signDataWithRSA+ :: SigType+ -> RSATypes.PrivateKey+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithRSA st prv has uhas payload =+ signRSAV4Core CleartextCompat st SHA512 has uhas prv payload++signDataWithRSAV6+ :: SigType+ -> SignatureSalt+ -> RSATypes.PrivateKey+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithRSAV6 st salt prv has uhas payload =+ signRSAV6Core CleartextCompat st SHA512 salt has uhas prv payload++-- FIXME: clean this up+ed25519Params+ , ed25519LegacyParams+ , ed448Params+ :: (String, Int, Int, PubKeyAlgorithm)+ed25519Params = ("Ed25519", 64, 32, PKA.Ed25519)+ed25519LegacyParams = ("Ed25519Legacy", 64, 32, PKA.EdDSA)+ed448Params = ("Ed448", 114, 57, PKA.Ed448)++signDataWithEdDSAV4Generic+ :: (String, Int, Int, PubKeyAlgorithm)+ -> (B.ByteString -> B.ByteString)+ -> TextNormalizationMode+ -> HashAlgorithm+ -> SigType+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithEdDSAV4Generic (algoName, sigLen, limbLen, pka) signer mode ha st has uhas payload =+ signEdDSAV4+ algoName+ sigLen+ limbLen+ signer+ mode+ st+ pka+ ha+ has+ uhas+ payload++signDataWithEdDSAV6Generic+ :: (String, Int, Int, PubKeyAlgorithm)+ -> (B.ByteString -> B.ByteString)+ -> TextNormalizationMode+ -> HashAlgorithm+ -> SigType+ -> SignatureSalt+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithEdDSAV6Generic (algoName, sigLen, limbLen, pka) signer mode ha st salt has uhas payload =+ signEdDSAV6+ algoName+ sigLen+ limbLen+ signer+ mode+ st+ pka+ ha+ salt+ has+ uhas+ payload++signDataWithEd25519+ :: SigType+ -> Ed25519.SecretKey+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithEd25519 st sk has uhas payload =+ signDataWithEdDSAV4Generic+ ed25519Params+ (ed25519Signer sk)+ CleartextCompat+ SHA512+ st+ has+ uhas+ payload++signDataWithEd25519Legacy+ :: SigType+ -> Ed25519.SecretKey+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithEd25519Legacy st sk has uhas payload =+ signDataWithEdDSAV4Generic+ ed25519LegacyParams+ (ed25519Signer sk)+ CleartextCompat+ SHA512+ st+ has+ uhas+ payload++signDataWithEd25519V6+ :: SigType+ -> SignatureSalt+ -> Ed25519.SecretKey+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithEd25519V6 st salt sk has uhas payload =+ signDataWithEdDSAV6Generic+ ed25519Params+ (ed25519Signer sk)+ CleartextCompat+ SHA512+ st+ salt+ has+ uhas+ payload++signDataWithEd448+ :: SigType+ -> Ed448.SecretKey+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithEd448 st sk has uhas payload =+ signDataWithEdDSAV4Generic+ ed448Params+ (ed448Signer sk)+ CleartextCompat+ SHA512+ st+ has+ uhas+ payload++signDataWithEd448V6+ :: SigType+ -> SignatureSalt+ -> Ed448.SecretKey+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithEd448V6 st salt sk has uhas payload =+ signDataWithEdDSAV6Generic+ ed448Params+ (ed448Signer sk)+ CleartextCompat+ SHA512+ st+ salt+ has+ uhas+ payload++{- | Builder-based signature creation for RSA++Example usage:+ builder <- sigBuilderInit BinarySig RSA SHA512+ builder' <- addHashedSubs hashedSubpackets builder+ builder'' <- addUnhashedSubs unhashedSubpackets builder'+ sig <- signDataWithRSABuilder builder'' rsaPrivateKey payload+-}+signDataWithRSABuilder+ :: SigBuilder+ Codec.Encryption.OpenPGP.Types.Unhashed+ Codec.Encryption.OpenPGP.Types.V4Sig+ 'PKA.RSA+ -> RSATypes.PrivateKey+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithRSABuilder builder prv payload =+ signRSAV4Core+ (sbTextNormMode builder)+ (sbSigType builder)+ (sbHashAlgo builder)+ (sbHashedSubs builder)+ (sbUnhashedSubs builder)+ prv+ payload++-- | Builder-based signature creation for RSA (v6)+signDataWithRSAV6Builder+ :: SigBuilder+ Codec.Encryption.OpenPGP.Types.Unhashed+ Codec.Encryption.OpenPGP.Types.V6Sig+ 'PKA.RSA+ -> RSATypes.PrivateKey+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithRSAV6Builder builder prv payload =+ signRSAV6Core+ (sbTextNormMode builder)+ (sbSigType builder)+ (sbHashAlgo builder)+ (sbSalt builder)+ (sbHashedSubs builder)+ (sbUnhashedSubs builder)+ prv+ payload++{- | Builder-based signature creation for Ed25519 (v4)++Example usage:+ builder <- sigBuilderInit BinarySig Ed25519 SHA512+ builder' <- addHashedSubs hashedSubpackets builder+ builder'' <- addUnhashedSubs unhashedSubpackets builder'+ sig <- signDataWithEd25519Builder builder'' ed25519PrivateKey payload+-}+signDataWithEd25519Builder+ :: SigBuilder+ Codec.Encryption.OpenPGP.Types.Unhashed+ Codec.Encryption.OpenPGP.Types.V4Sig+ 'PKA.Ed25519+ -> Ed25519.SecretKey+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithEd25519Builder builder sk payload =+ signDataWithEdDSAV4Generic+ ed25519Params+ (ed25519Signer sk)+ (sbTextNormMode builder)+ (sbHashAlgo builder)+ (sbSigType builder)+ (sbHashedSubs builder)+ (sbUnhashedSubs builder)+ payload++{- | Builder-based signature creation for Ed25519 (v6)++Example usage:+ builder <- sigBuilderInitV6 BinarySig Ed25519 SHA512 salt+ builder' <- addHashedSubs hashedSubpackets builder+ builder'' <- addUnhashedSubs unhashedSubpackets builder'+ sig <- signDataWithEd25519V6Builder builder'' ed25519PrivateKey payload+-}+signDataWithEd25519V6Builder+ :: SigBuilder+ Codec.Encryption.OpenPGP.Types.Unhashed+ Codec.Encryption.OpenPGP.Types.V6Sig+ 'PKA.Ed25519+ -> Ed25519.SecretKey+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithEd25519V6Builder builder sk payload =+ signDataWithEdDSAV6Generic+ ed25519Params+ (ed25519Signer sk)+ (sbTextNormMode builder)+ (sbHashAlgo builder)+ (sbSigType builder)+ (sbSalt builder)+ (sbHashedSubs builder)+ (sbUnhashedSubs builder)+ payload++{- | Builder-based signature creation for Ed448 (v4)++Example usage:+ builder <- sigBuilderInit BinarySig Ed448 SHA512+ builder' <- addHashedSubs hashedSubpackets builder+ builder'' <- addUnhashedSubs unhashedSubpackets builder'+ sig <- signDataWithEd448Builder builder'' ed448PrivateKey payload+-}+signDataWithEd448Builder+ :: SigBuilder+ Codec.Encryption.OpenPGP.Types.Unhashed+ Codec.Encryption.OpenPGP.Types.V4Sig+ 'PKA.Ed448+ -> Ed448.SecretKey+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithEd448Builder builder sk payload =+ signDataWithEdDSAV4Generic+ ed448Params+ (ed448Signer sk)+ (sbTextNormMode builder)+ (sbHashAlgo builder)+ (sbSigType builder)+ (sbHashedSubs builder)+ (sbUnhashedSubs builder)+ payload++-- | Builder-based signature creation for Ed448 (v6)+signDataWithEd448V6Builder+ :: SigBuilder+ Codec.Encryption.OpenPGP.Types.Unhashed+ Codec.Encryption.OpenPGP.Types.V6Sig+ 'PKA.Ed448+ -> Ed448.SecretKey+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithEd448V6Builder builder sk payload =+ signDataWithEdDSAV6Generic+ ed448Params+ (ed448Signer sk)+ (sbTextNormMode builder)+ (sbHashAlgo builder)+ (sbSigType builder)+ (sbSalt builder)+ (sbHashedSubs builder)+ (sbUnhashedSubs builder)+ payload++{- | Algorithm-agnostic signature builder dispatcher (Phase 2)++Dispatches to the appropriate signing function based on the private key type.+The private key type (PrivateKeyFor algo) encodes the algorithm at the type level,+allowing compile-time verification that the key and builder algorithm match.++Example usage:+-}+class BuilderSigningAlgorithm (algo :: PKA.PubKeyAlgorithm) where+ signDataWithAlgorithmicBuilderImpl+ :: SigBuilder+ Codec.Encryption.OpenPGP.Types.Unhashed+ Codec.Encryption.OpenPGP.Types.V4Sig+ algo+ -> PrivateKeyFor algo+ -> ByteString+ -> Either SignError SignaturePayload++instance BuilderSigningAlgorithm 'PKA.RSA where+ signDataWithAlgorithmicBuilderImpl builder (SP.RSAPrivateKey prv) payload =+ signDataWithRSABuilder builder prv payload++instance BuilderSigningAlgorithm 'PKA.Ed25519 where+ signDataWithAlgorithmicBuilderImpl builder (SP.Ed25519PrivateKey sk) payload =+ signDataWithEd25519Builder builder sk payload++instance BuilderSigningAlgorithm 'PKA.Ed448 where+ signDataWithAlgorithmicBuilderImpl builder (SP.Ed448PrivateKey sk) payload =+ signDataWithEd448Builder builder sk payload++{- | Catch-all instance that produces a compile-time error for any algorithm+that is not supported by the algorithmic builder (e.g. DSA, ECDSA).+-}+instance+ {-# OVERLAPPABLE #-}+ ( TypeError+ ( 'Text+ "signDataWithAlgorithmicBuilder does not support this algorithm."+ ':$$: 'Text "Supported algorithms: RSA, Ed25519, Ed448."+ ':$$: 'Text+ "For DSA or ECDSA, use signDataWith{DSA,ECDSA}Builder directly."+ )+ )+ => BuilderSigningAlgorithm algo+ where+ signDataWithAlgorithmicBuilderImpl = error "unreachable: TypeError fires at compile time"++signDataWithAlgorithmicBuilder+ :: forall (algo :: PKA.PubKeyAlgorithm)+ . BuilderSigningAlgorithm algo+ => SigBuilder+ Codec.Encryption.OpenPGP.Types.Unhashed+ Codec.Encryption.OpenPGP.Types.V4Sig+ algo+ -> PrivateKeyFor algo+ -> ByteString+ -> Either SignError SignaturePayload+signDataWithAlgorithmicBuilder =+ signDataWithAlgorithmicBuilderImpl
Codec/Encryption/OpenPGP/Types.hs view
@@ -4,10 +4,10 @@ -- (See the LICENSE file). module Codec.Encryption.OpenPGP.Types- ( module X- ) where+ ( module X+ ) where -import Codec.Encryption.OpenPGP.Types.Internal.Base as X hiding (Ed25519, Ed448)+import Codec.Encryption.OpenPGP.Types.Internal.Base as X import Codec.Encryption.OpenPGP.Types.Internal.CryptonNewtypes as X import Codec.Encryption.OpenPGP.Types.Internal.PKITypes as X import Codec.Encryption.OpenPGP.Types.Internal.PacketClass as X
Codec/Encryption/OpenPGP/Types/Internal/Base.hs view
@@ -2,1770 +2,2053 @@ -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).--{-# LANGUAGE CPP #-}-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE PatternSynonyms #-}-{-# LANGUAGE StandaloneDeriving #-}-{-# LANGUAGE TemplateHaskell #-}--module Codec.Encryption.OpenPGP.Types.Internal.Base where--import GHC.Generics (Generic)--import Codec.Encryption.OpenPGP.Types.Internal.PrettyUtils (prettyLBS)-import Control.Applicative ((<|>))-import Control.Arrow ((***))-import Control.Lens (makeLenses, op, Wrapped)-import Control.Monad (mzero)-import Data.Bits ((.&.))-import Data.Aeson ((.=), object)-import qualified Data.Aeson as A-import qualified Data.Aeson.Key as AK-import qualified Data.Aeson.TH as ATH-import Data.ByteArray (ByteArrayAccess)-import qualified Data.ByteString as B-import qualified Data.ByteString.Base16.Lazy as B16L-import Data.ByteString.Lazy (ByteString)-import qualified Data.ByteString.Lazy as BL-import qualified Data.ByteString.Lazy.Char8 as BLC8-import Data.Char (toLower, toUpper)-import Data.Data (Data)-import Data.Hashable (Hashable(..))-import Data.Int (Int64)-import Data.Kind (Type)-import Data.List (unfoldr)-import Data.List.NonEmpty (NonEmpty)-import qualified Data.List.NonEmpty as NE-import Data.List.Split (chunksOf)-import Data.Maybe (fromMaybe)-import Data.Ord (comparing)-import Data.Set (Set)-import qualified Data.Set as Set-import Data.Text (Text)-import qualified Data.Text as T-import Data.Time.Clock.POSIX (posixSecondsToUTCTime)-import Data.Time.Format (formatTime)-import Data.Time.Locale.Compat (defaultTimeLocale)-import Data.Typeable (Typeable)-import Data.Word (Word16, Word32, Word8)-import Network.URI (URI(..), nullURI, parseURI, uriToString)-import Numeric (readHex)-import Prettyprinter (Pretty(..), (<+>), hsep, punctuate, space)-import Data.IORef (IORef, atomicModifyIORef', newIORef)-import System.IO.Unsafe (unsafePerformIO)--type Exportability = Bool--type TrustLevel = Word8--type TrustAmount = Word8--type AlmostPublicDomainRegex = ByteString--type Revocability = Bool--type RevocationReason = Text--type KeyServer = ByteString--type SignatureHash = ByteString--type PacketVersion = Word8--type V3Expiration = Word16--type CompressedDataPayload = ByteString--type FileName = ByteString--type ImageData = ByteString--type NestedFlag = Bool---- | Phantom types for tracking subpacket classification and signature version--- These types are never instantiated; they exist purely for compile-time type safety.---- | Phantom marker for hashed subpackets (included in signature hash computation)-data Hashed---- | Phantom marker for unhashed subpackets (not included in signature hash computation)-data Unhashed---- | Phantom marker for v4 signatures (8-octet issuer, no salt)-data V4Sig---- | Phantom marker for v6 signatures (fingerprint issuer, requires salt)-data V6Sig--data ByteRange =- ByteRange- { _rangeOffset :: Int64- , _rangeLength :: Int64- }- deriving (Data, Eq, Generic, Ord, Show, Typeable)--newtype WireRepSourceId =- WireRepSourceId- { _unWireRepSourceId :: Int64- }- deriving (Data, Eq, Generic, Ord, Show, Typeable)--data WireRepRef =- WireRepRef- { _wireRepSourceId :: WireRepSourceId- , _wireRepLength :: Int64- , _wireRepName :: Maybe Text- , _wireRepWasOriginallyArmored :: Bool- }- deriving (Data, Eq, Generic, Ord, Show, Typeable)--type WireRepRefs = NonEmpty WireRepRef--wireRepRef :: ByteString -> WireRepRef-wireRepRef = mkWireRepRefWithLength Nothing False . BL.length--namedWireRepRef :: Text -> ByteString -> WireRepRef-namedWireRepRef name = mkWireRepRefWithLength (Just name) False . BL.length--mkWireRepRefWithLength ::- Maybe Text -> Bool -> Int64 -> WireRepRef-mkWireRepRefWithLength mname wasOriginallyArmored payloadLen =- WireRepRef- { _wireRepSourceId = freshWireRepSourceId payloadLen- , _wireRepLength = payloadLen- , _wireRepName = mname- , _wireRepWasOriginallyArmored = wasOriginallyArmored- }--mkWireRepRef :: Maybe Text -> Bool -> ByteString -> WireRepRef-mkWireRepRef mname wasOriginallyArmored =- mkWireRepRefWithLength mname wasOriginallyArmored . BL.length--wireRepSourceCounter :: IORef Int64-wireRepSourceCounter = unsafePerformIO (newIORef 0)-{-# NOINLINE wireRepSourceCounter #-}--freshWireRepSourceId :: Int64 -> WireRepSourceId-freshWireRepSourceId !_ =- unsafePerformIO $- atomicModifyIORef' wireRepSourceCounter (\n -> let n' = n + 1 in (n', WireRepSourceId n'))-{-# NOINLINE freshWireRepSourceId #-}--rangeEnd :: ByteRange -> Int64-rangeEnd r = _rangeOffset r + _rangeLength r--spanByteRanges :: [ByteRange] -> Maybe ByteRange-spanByteRanges [] = Nothing-spanByteRanges (r:rs) =- let start = minimum (_rangeOffset <$> (r : rs))- ending = maximum (rangeEnd <$> (r : rs))- in Just (ByteRange start (ending - start))--$(makeLenses ''ByteRange)-$(makeLenses ''WireRepRef)--class (Eq a, Ord a) =>- FutureFlag a- where- fromFFlag :: a -> Int- toFFlag :: Int -> a--class (Eq a, Ord a) =>- FutureVal a- where- fromFVal :: a -> Word8- toFVal :: Word8 -> a--data SymmetricAlgorithm- = Plaintext- | IDEA- | TripleDES- | CAST5- | Blowfish- | ReservedSAFER- | ReservedDES- | AES128- | AES192- | AES256- | Twofish- | Camellia128- | Camellia192- | Camellia256- | OtherSA Word8- deriving (Data, Generic, Show, Typeable)--instance Eq SymmetricAlgorithm where- (==) a b = fromFVal a == fromFVal b--instance Ord SymmetricAlgorithm where- compare = comparing fromFVal--instance FutureVal SymmetricAlgorithm where- fromFVal Plaintext = 0- fromFVal IDEA = 1- fromFVal TripleDES = 2- fromFVal CAST5 = 3- fromFVal Blowfish = 4- fromFVal ReservedSAFER = 5- fromFVal ReservedDES = 6- fromFVal AES128 = 7- fromFVal AES192 = 8- fromFVal AES256 = 9- fromFVal Twofish = 10- fromFVal Camellia128 = 11- fromFVal Camellia192 = 12- fromFVal Camellia256 = 13- fromFVal (OtherSA o) = o- toFVal 0 = Plaintext- toFVal 1 = IDEA- toFVal 2 = TripleDES- toFVal 3 = CAST5- toFVal 4 = Blowfish- toFVal 5 = ReservedSAFER- toFVal 6 = ReservedDES- toFVal 7 = AES128- toFVal 8 = AES192- toFVal 9 = AES256- toFVal 10 = Twofish- toFVal 11 = Camellia128- toFVal 12 = Camellia192- toFVal 13 = Camellia256- toFVal o = OtherSA o--instance Hashable SymmetricAlgorithm--instance Pretty SymmetricAlgorithm where- pretty Plaintext = pretty "plaintext"- pretty IDEA = pretty "IDEA"- pretty TripleDES = pretty "3DES"- pretty CAST5 = pretty "CAST-128"- pretty Blowfish = pretty "Blowfish"- pretty ReservedSAFER = pretty "(reserved) SAFER"- pretty ReservedDES = pretty "(reserved) DES"- pretty AES128 = pretty "AES-128"- pretty AES192 = pretty "AES-192"- pretty AES256 = pretty "AES-256"- pretty Twofish = pretty "Twofish"- pretty Camellia128 = pretty "Camellia-128"- pretty Camellia192 = pretty "Camellia-192"- pretty Camellia256 = pretty "Camellia-256"- pretty (OtherSA sa) = pretty "unknown symmetric algorithm" <+> pretty sa--$(ATH.deriveJSON ATH.defaultOptions ''SymmetricAlgorithm)--data NotationFlag- = HumanReadable- | OtherNF Word8- deriving (Data, Generic, Show, Typeable)--mkNotationFlag :: Word8 -> NotationFlag-mkNotationFlag o- | o' == 0 = HumanReadable- | otherwise = OtherNF o'- where- o' = o .&. 0x0f--instance Eq NotationFlag where- (==) a b = fromFFlag a == fromFFlag b--instance Ord NotationFlag where- compare = comparing fromFFlag--instance FutureFlag NotationFlag where- fromFFlag HumanReadable = 0- fromFFlag (OtherNF o) = fromIntegral (o .&. 0x0f)- toFFlag 0 = HumanReadable- toFFlag o = mkNotationFlag (fromIntegral o)--instance Hashable NotationFlag--instance Pretty NotationFlag where- pretty HumanReadable = pretty "human-readable"- pretty (OtherNF o) = pretty "unknown notation flag type" <+> pretty o--$(ATH.deriveJSON ATH.defaultOptions ''NotationFlag)--newtype ThirtyTwoBitTimeStamp =- ThirtyTwoBitTimeStamp- { unThirtyTwoBitTimeStamp :: Word32- }- deriving ( Bounded- , Data- , Enum- , Eq- , Generic- , Hashable- , Integral- , Num- , Ord- , Real- , Show- , Typeable- )--instance Wrapped ThirtyTwoBitTimeStamp--instance Pretty ThirtyTwoBitTimeStamp where- pretty =- pretty .- formatTime defaultTimeLocale "%Y%m%d-%H%M%S" .- posixSecondsToUTCTime . realToFrac--$(ATH.deriveJSON ATH.defaultOptions ''ThirtyTwoBitTimeStamp)--durU :: (Integral a, Show a) => a -> Maybe (String, a)-durU x- | x >= 31557600 = Just ((++ "y") . show $ x `div` 31557600, x `mod` 31557600)- | x >= 2629800 = Just ((++ "m") . show $ x `div` 2629800, x `mod` 2629800)- | x >= 86400 = Just ((++ "d") . show $ x `div` 86400, x `mod` 86400)- | x > 0 = Just ((++ "s") . show $ x, 0)- | otherwise = Nothing--newtype ThirtyTwoBitDuration =- ThirtyTwoBitDuration- { unThirtyTwoBitDuration :: Word32- }- deriving ( Bounded- , Data- , Enum- , Eq- , Generic- , Hashable- , Integral- , Num- , Ord- , Real- , Show- , Typeable- )--instance Wrapped ThirtyTwoBitDuration--instance Pretty ThirtyTwoBitDuration where- pretty = pretty . concat . unfoldr durU . op ThirtyTwoBitDuration--$(ATH.deriveJSON ATH.defaultOptions ''ThirtyTwoBitDuration)--data RevocationClass- = SensitiveRK- | RClOther Word8- deriving (Data, Generic, Show, Typeable)--mkRevocationClass :: Word8 -> RevocationClass-mkRevocationClass i- | i' == 1 = SensitiveRK- | otherwise = RClOther i'- where- i' = i .&. 0x07--instance Eq RevocationClass where- (==) a b = fromFFlag a == fromFFlag b--instance Ord RevocationClass where- compare = comparing fromFFlag--instance FutureFlag RevocationClass where- fromFFlag SensitiveRK = 1- fromFFlag (RClOther i) = fromIntegral (i .&. 0x07)- toFFlag 1 = SensitiveRK- toFFlag i = mkRevocationClass (fromIntegral i)--instance Hashable RevocationClass--instance Pretty RevocationClass where- pretty SensitiveRK = pretty "sensitive"- pretty (RClOther o) = pretty "unknown revocation class" <+> pretty o--$(ATH.deriveJSON ATH.defaultOptions ''RevocationClass)--data PubKeyAlgorithm- = RSA- | DeprecatedRSAEncryptOnly- | DeprecatedRSASignOnly- | ElgamalEncryptOnly- | DSA- | ECDH- | ECDSA- | ForbiddenElgamal- | DH- | EdDSA- | X25519- | X448- | Ed25519- | Ed448- | OtherPKA Word8- deriving (Show, Data, Generic, Typeable)--instance Eq PubKeyAlgorithm where- (==) a b = fromFVal a == fromFVal b--instance Ord PubKeyAlgorithm where- compare = comparing fromFVal--instance FutureVal PubKeyAlgorithm where- fromFVal RSA = 1- fromFVal DeprecatedRSAEncryptOnly = 2- fromFVal DeprecatedRSASignOnly = 3- fromFVal ElgamalEncryptOnly = 16- fromFVal DSA = 17- fromFVal ECDH = 18- fromFVal ECDSA = 19- fromFVal ForbiddenElgamal = 20- fromFVal DH = 21- fromFVal EdDSA = 22- fromFVal X25519 = 25- fromFVal X448 = 26- fromFVal Ed25519 = 27- fromFVal Ed448 = 28- fromFVal (OtherPKA o) = o- toFVal 1 = RSA- toFVal 2 = DeprecatedRSAEncryptOnly- toFVal 3 = DeprecatedRSASignOnly- toFVal 16 = ElgamalEncryptOnly- toFVal 17 = DSA- toFVal 18 = ECDH- toFVal 19 = ECDSA- toFVal 20 = ForbiddenElgamal- toFVal 21 = DH- toFVal 22 = EdDSA- toFVal 25 = X25519- toFVal 26 = X448- toFVal 27 = Ed25519- toFVal 28 = Ed448- toFVal o = OtherPKA o--instance Hashable PubKeyAlgorithm--instance Pretty PubKeyAlgorithm where- pretty RSA = pretty "RSA"- pretty DeprecatedRSAEncryptOnly = pretty "(deprecated) RSA encrypt-only"- pretty DeprecatedRSASignOnly = pretty "(deprecated) RSA sign-only"- pretty ElgamalEncryptOnly = pretty "Elgamal encrypt-only"- pretty DSA = pretty "DSA"- pretty ECDH = pretty "ECDH"- pretty ECDSA = pretty "ECDSA"- pretty ForbiddenElgamal = pretty "(forbidden) Elgamal"- pretty DH = pretty "DH"- pretty EdDSA = pretty "EdDSA"- pretty X25519 = pretty "X25519"- pretty X448 = pretty "X448"- pretty Ed25519 = pretty "Ed25519"- pretty Ed448 = pretty "Ed448"- pretty (OtherPKA pka) = pretty "unknown pubkey algorithm type" <+> pretty pka--$(ATH.deriveJSON ATH.defaultOptions ''PubKeyAlgorithm)---- | An OpenPGP fingerprint. Length depends on key version:--- 16 bytes (v3/MD5), 20 bytes (v4/SHA-1), or 32 bytes (v6/SHA-256).-newtype Fingerprint =- Fingerprint- { unFingerprint :: ByteString- }- deriving (Data, Eq, Generic, Ord, Show, Typeable)--instance Wrapped Fingerprint--instance Read Fingerprint where- readsPrec _ s =- let ws = hexToW8s (filter (/= ' ') s)- in if null ws- then []- else [(Fingerprint (BL.pack (map fst ws)), snd (last ws))]--instance Hashable Fingerprint--instance Pretty Fingerprint where- pretty = pretty . bsToHexUpper . unFingerprint---- FIXME: we should not be exporting this-key :: String -> AK.Key-key = AK.fromText . T.pack--instance A.ToJSON Fingerprint where- toJSON e = object [key "fpr" .= (A.toJSON . show . pretty) e]--instance A.FromJSON Fingerprint where- parseJSON (A.Object v) = Fingerprint . read <$> v A..: key "fpr"- parseJSON _ = mzero--newtype SpacedFingerprint =- SpacedFingerprint- { unSpacedFingerprint :: Fingerprint- }- deriving (Data, Eq, Generic, Ord, Show, Typeable)--instance Wrapped SpacedFingerprint--instance Pretty SpacedFingerprint where- pretty =- hsep .- punctuate space .- map hsep .- chunksOf 5 .- map pretty . chunksOf 4 . bsToHexUpper . unFingerprint . op SpacedFingerprint--bsToHexUpper :: ByteString -> String-bsToHexUpper = map toUpper . BLC8.unpack . B16L.encode--hexToW8s :: ReadS Word8-hexToW8s = concatMap readHex . chunksOf 2 . map toLower--newtype EightOctetKeyId =- EightOctetKeyId- { unEOKI :: ByteString- }- deriving (Data, Eq, Generic, Ord, Typeable)--instance Wrapped EightOctetKeyId--instance Pretty EightOctetKeyId where- pretty = pretty . bsToHexUpper . op EightOctetKeyId--instance Show EightOctetKeyId where- show = bsToHexUpper . op EightOctetKeyId--instance Read EightOctetKeyId where- readsPrec _ =- map ((EightOctetKeyId . BL.pack *** concat) . unzip) . chunksOf 8 . hexToW8s--instance Hashable EightOctetKeyId--instance A.ToJSON EightOctetKeyId where- toJSON e = object [key "eoki" .= (bsToHexUpper . op EightOctetKeyId) e]--instance A.FromJSON EightOctetKeyId where- parseJSON (A.Object v) = EightOctetKeyId . read <$> v A..: key "eoki"- parseJSON _ = mzero--data KeyIdentifier- = KeyIdentifierWildcard- | KeyIdentifierEightOctet EightOctetKeyId- | KeyIdentifierFingerprint Fingerprint- deriving (Data, Eq, Generic, Ord, Show, Typeable)--instance Pretty KeyIdentifier where- pretty KeyIdentifierWildcard = pretty "wildcard"- pretty (KeyIdentifierEightOctet kid) = pretty "key-id" <+> pretty kid- pretty (KeyIdentifierFingerprint fp) = pretty "fingerprint" <+> pretty fp--instance A.ToJSON KeyIdentifier where- toJSON KeyIdentifierWildcard = object [key "wildcard" .= A.Bool True]- toJSON (KeyIdentifierEightOctet kid) = object [key "keyId" .= kid]- toJSON (KeyIdentifierFingerprint fp) = object [key "fingerprint" .= fp]--instance A.FromJSON KeyIdentifier where- parseJSON (A.Object v) =- ((v A..: key "wildcard") >>= \isWildcard ->- if isWildcard then pure KeyIdentifierWildcard else mzero)- <|> (KeyIdentifierEightOctet <$> v A..: key "keyId")- <|> (KeyIdentifierFingerprint <$> v A..: key "fingerprint")- parseJSON _ = mzero--newtype NotationName =- NotationName- { unNotationName :: ByteString- }- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)--instance Pretty NotationName where- pretty = prettyLBS . unNotationName--instance Wrapped NotationName--instance A.ToJSON NotationName where- toJSON nn = object [key "notationname" .= show (op NotationName nn)]--instance A.FromJSON NotationName where- parseJSON (A.Object v) = NotationName . read <$> v A..: key "notationname"- parseJSON _ = mzero--newtype NotationValue =- NotationValue- { unNotationValue :: ByteString- }- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)--instance Pretty NotationValue where- pretty = prettyLBS . unNotationValue--instance Wrapped NotationValue--instance A.ToJSON NotationValue where- toJSON nv = object [key "notationvalue" .= show (op NotationValue nv)]--instance A.FromJSON NotationValue where- parseJSON (A.Object v) =- NotationValue . read <$> v A..: key "notationvalue"- parseJSON _ = mzero--data HashAlgorithm- = DeprecatedMD5- | SHA1- | RIPEMD160- | SHA256- | SHA384- | SHA512- | SHA224- | SHA3_256- | SHA3_512- | OtherHA Word8- deriving (Data, Generic, Show, Typeable)--instance Eq HashAlgorithm where- (==) a b = fromFVal a == fromFVal b--instance Ord HashAlgorithm where- compare = comparing fromFVal--instance FutureVal HashAlgorithm where- fromFVal DeprecatedMD5 = 1- fromFVal SHA1 = 2- fromFVal RIPEMD160 = 3- fromFVal SHA256 = 8- fromFVal SHA384 = 9- fromFVal SHA512 = 10- fromFVal SHA224 = 11- fromFVal SHA3_256 = 12- fromFVal SHA3_512 = 14- fromFVal (OtherHA o) = o- toFVal 1 = DeprecatedMD5- toFVal 2 = SHA1- toFVal 3 = RIPEMD160- toFVal 8 = SHA256- toFVal 9 = SHA384- toFVal 10 = SHA512- toFVal 11 = SHA224- toFVal 12 = SHA3_256- toFVal 14 = SHA3_512- toFVal o = OtherHA o--instance Hashable HashAlgorithm--instance Pretty HashAlgorithm where- pretty DeprecatedMD5 = pretty "(deprecated) MD5"- pretty SHA1 = pretty "SHA-1"- pretty RIPEMD160 = pretty "RIPEMD-160"- pretty SHA256 = pretty "SHA-256"- pretty SHA384 = pretty "SHA-384"- pretty SHA512 = pretty "SHA-512"- pretty SHA224 = pretty "SHA-224"- pretty SHA3_256 = pretty "SHA3-256"- pretty SHA3_512 = pretty "SHA3-512"- pretty (OtherHA ha) = pretty "unknown hash algorithm type" <+> pretty ha--$(ATH.deriveJSON ATH.defaultOptions ''HashAlgorithm)--data CompressionAlgorithm- = Uncompressed- | ZIP- | ZLIB- | BZip2- | OtherCA Word8- deriving (Show, Data, Generic, Typeable)--instance Eq CompressionAlgorithm where- (==) a b = fromFVal a == fromFVal b--instance Ord CompressionAlgorithm where- compare = comparing fromFVal--instance FutureVal CompressionAlgorithm where- fromFVal Uncompressed = 0- fromFVal ZIP = 1- fromFVal ZLIB = 2- fromFVal BZip2 = 3- fromFVal (OtherCA o) = o- toFVal 0 = Uncompressed- toFVal 1 = ZIP- toFVal 2 = ZLIB- toFVal 3 = BZip2- toFVal o = OtherCA o--instance Hashable CompressionAlgorithm--instance Pretty CompressionAlgorithm where- pretty Uncompressed = pretty "uncompressed"- pretty ZIP = pretty "ZIP"- pretty ZLIB = pretty "zlib"- pretty BZip2 = pretty "bzip2"- pretty (OtherCA ca) =- pretty "unknown compression algorithm type" <+> pretty ca--$(ATH.deriveJSON ATH.defaultOptions ''CompressionAlgorithm)--data AEADAlgorithm- = EAX- | OCB- | GCM- | OtherAEADAlgo Word8- deriving (Show, Data, Generic, Typeable)--instance Eq AEADAlgorithm where- (==) a b = fromFVal a == fromFVal b--instance Ord AEADAlgorithm where- compare = comparing fromFVal--instance FutureVal AEADAlgorithm where- fromFVal EAX = 1- fromFVal OCB = 2- fromFVal GCM = 3- fromFVal (OtherAEADAlgo o) = o- toFVal 1 = EAX- toFVal 2 = OCB- toFVal 3 = GCM- toFVal o = OtherAEADAlgo o--instance Hashable AEADAlgorithm--instance Pretty AEADAlgorithm where- pretty EAX = pretty "EAX"- pretty OCB = pretty "OCB"- pretty GCM = pretty "GCM"- pretty (OtherAEADAlgo aa) = pretty "unknown AEAD algorithm type" <+> pretty aa--$(ATH.deriveJSON ATH.defaultOptions ''AEADAlgorithm)--data KSPFlag- = NoModify- | KSPOther Int- deriving (Data, Generic, Show, Typeable)--instance Eq KSPFlag where- (==) a b = fromFFlag a == fromFFlag b--instance Ord KSPFlag where- compare = comparing fromFFlag--instance FutureFlag KSPFlag where- fromFFlag NoModify = 0- fromFFlag (KSPOther i) = fromIntegral i- toFFlag 0 = NoModify- toFFlag i = KSPOther (fromIntegral i)--instance Hashable KSPFlag--instance Pretty KSPFlag where- pretty NoModify = pretty "no-modify"- pretty (KSPOther o) =- pretty "unknown keyserver preference flag type" <+> pretty o--$(ATH.deriveJSON ATH.defaultOptions ''KSPFlag)--data KeyFlag- = GroupKey- | AuthKey- | SplitKey- | EncryptStorageKey- | EncryptCommunicationsKey- | SignDataKey- | CertifyKeysKey- | KFOther Int- deriving (Data, Generic, Show, Typeable)--instance Eq KeyFlag where- (==) a b = fromFFlag a == fromFFlag b--instance Ord KeyFlag where- compare = comparing fromFFlag--instance FutureFlag KeyFlag where- fromFFlag GroupKey = 0- fromFFlag AuthKey = 2- fromFFlag SplitKey = 3- fromFFlag EncryptStorageKey = 4- fromFFlag EncryptCommunicationsKey = 5- fromFFlag SignDataKey = 6- fromFFlag CertifyKeysKey = 7- fromFFlag (KFOther i) = fromIntegral i- toFFlag 0 = GroupKey- toFFlag 2 = AuthKey- toFFlag 3 = SplitKey- toFFlag 4 = EncryptStorageKey- toFFlag 5 = EncryptCommunicationsKey- toFFlag 6 = SignDataKey- toFFlag 7 = CertifyKeysKey- toFFlag i = KFOther (fromIntegral i)--instance Hashable KeyFlag--instance Pretty KeyFlag where- pretty GroupKey = pretty "group"- pretty AuthKey = pretty "auth"- pretty SplitKey = pretty "split"- pretty EncryptStorageKey = pretty "encrypt-storage"- pretty EncryptCommunicationsKey = pretty "encrypt-communications"- pretty SignDataKey = pretty "sign-data"- pretty CertifyKeysKey = pretty "certify-keys"- pretty (KFOther o) = pretty "unknown key flag type" <+> pretty o--$(ATH.deriveJSON ATH.defaultOptions ''KeyFlag)--data RevocationCode- = NoReason- | KeySuperseded- | KeyMaterialCompromised- | KeyRetiredAndNoLongerUsed- | UserIdInfoNoLongerValid- | RCoOther Word8- deriving (Data, Generic, Show, Typeable)--instance Eq RevocationCode where- (==) a b = fromFVal a == fromFVal b--instance Ord RevocationCode where- compare = comparing fromFVal--instance FutureVal RevocationCode where- fromFVal NoReason = 0- fromFVal KeySuperseded = 1- fromFVal KeyMaterialCompromised = 2- fromFVal KeyRetiredAndNoLongerUsed = 3- fromFVal UserIdInfoNoLongerValid = 32- fromFVal (RCoOther o) = o- toFVal 0 = NoReason- toFVal 1 = KeySuperseded- toFVal 2 = KeyMaterialCompromised- toFVal 3 = KeyRetiredAndNoLongerUsed- toFVal 32 = UserIdInfoNoLongerValid- toFVal o = RCoOther o--instance Hashable RevocationCode--instance Pretty RevocationCode where- pretty NoReason = pretty "no reason"- pretty KeySuperseded = pretty "key superseded"- pretty KeyMaterialCompromised = pretty "key material compromised"- pretty KeyRetiredAndNoLongerUsed = pretty "key retired and no longer used"- pretty UserIdInfoNoLongerValid = pretty "user-ID info no longer valid"- pretty (RCoOther o) = pretty "unknown revocation code" <+> pretty o--$(ATH.deriveJSON ATH.defaultOptions ''RevocationCode)--data FeatureFlag- = FeatureSEIPDv1- | FeatureSEIPDv2- | FeatureOther Int- deriving (Data, Generic, Show, Typeable)--instance Eq FeatureFlag where- (==) a b = fromFFlag a == fromFFlag b--instance Ord FeatureFlag where- compare = comparing fromFFlag--instance FutureFlag FeatureFlag where- fromFFlag FeatureSEIPDv1 = 7- fromFFlag FeatureSEIPDv2 = 4- fromFFlag (FeatureOther i) = fromIntegral i- toFFlag 7 = FeatureSEIPDv1- toFFlag 4 = FeatureSEIPDv2- toFFlag i = FeatureOther (fromIntegral i)--instance Hashable FeatureFlag--instance Pretty FeatureFlag where- pretty FeatureSEIPDv1 = pretty "seipd-v1"- pretty FeatureSEIPDv2 = pretty "seipd-v2"- pretty (FeatureOther o) = pretty "unknown feature flag type" <+> pretty o--$(ATH.deriveJSON ATH.defaultOptions ''FeatureFlag)--newtype URL =- URL- { unURL :: URI- }- deriving (Data, Eq, Generic, Ord, Show, Typeable)--instance Wrapped URL--instance Hashable URL where- hashWithSalt salt (URL (URI s a p q f)) =- salt `hashWithSalt` s `hashWithSalt` show a `hashWithSalt` p `hashWithSalt`- q `hashWithSalt`- f--instance Pretty URL where- pretty = pretty . (\uri -> uriToString id uri "") . op URL--instance A.ToJSON URL where- toJSON u = object [key "uri" .= (\uri -> uriToString id uri "") (op URL u)]--instance A.FromJSON URL where- parseJSON (A.Object v) =- URL . fromMaybe nullURI . parseURI <$> v A..: key "uri"- parseJSON _ = mzero--data SigType- = BinarySig- | CanonicalTextSig- | StandaloneSig- | GenericCert- | PersonaCert- | CasualCert- | PositiveCert- | SubkeyBindingSig- | PrimaryKeyBindingSig- | SignatureDirectlyOnAKey- | KeyRevocationSig- | SubkeyRevocationSig- | CertRevocationSig- | TimestampSig- | ThirdPartyConfirmationSig- | OtherSig Word8- deriving (Data, Generic, Show, Typeable)--instance Eq SigType where- (==) a b = fromFVal a == fromFVal b--instance Ord SigType where- compare = comparing fromFVal--instance FutureVal SigType where- fromFVal BinarySig = 0x00- fromFVal CanonicalTextSig = 0x01- fromFVal StandaloneSig = 0x02- fromFVal GenericCert = 0x10- fromFVal PersonaCert = 0x11- fromFVal CasualCert = 0x12- fromFVal PositiveCert = 0x13- fromFVal SubkeyBindingSig = 0x18- fromFVal PrimaryKeyBindingSig = 0x19- fromFVal SignatureDirectlyOnAKey = 0x1F- fromFVal KeyRevocationSig = 0x20- fromFVal SubkeyRevocationSig = 0x28- fromFVal CertRevocationSig = 0x30- fromFVal TimestampSig = 0x40- fromFVal ThirdPartyConfirmationSig = 0x50- fromFVal (OtherSig o) = o- toFVal 0x00 = BinarySig- toFVal 0x01 = CanonicalTextSig- toFVal 0x02 = StandaloneSig- toFVal 0x10 = GenericCert- toFVal 0x11 = PersonaCert- toFVal 0x12 = CasualCert- toFVal 0x13 = PositiveCert- toFVal 0x18 = SubkeyBindingSig- toFVal 0x19 = PrimaryKeyBindingSig- toFVal 0x1F = SignatureDirectlyOnAKey- toFVal 0x20 = KeyRevocationSig- toFVal 0x28 = SubkeyRevocationSig- toFVal 0x30 = CertRevocationSig- toFVal 0x40 = TimestampSig- toFVal 0x50 = ThirdPartyConfirmationSig- toFVal o = OtherSig o--instance Hashable SigType--instance Pretty SigType where- pretty BinarySig = pretty "binary"- pretty CanonicalTextSig = pretty "canonical-pretty"- pretty StandaloneSig = pretty "standalone"- pretty GenericCert = pretty "generic"- pretty PersonaCert = pretty "persona"- pretty CasualCert = pretty "casual"- pretty PositiveCert = pretty "positive"- pretty SubkeyBindingSig = pretty "subkey-binding"- pretty PrimaryKeyBindingSig = pretty "primary-key-binding"- pretty SignatureDirectlyOnAKey = pretty "signature directly on a key"- pretty KeyRevocationSig = pretty "key-revocation"- pretty SubkeyRevocationSig = pretty "subkey-revocation"- pretty CertRevocationSig = pretty "cert-revocation"- pretty TimestampSig = pretty "timestamp"- pretty ThirdPartyConfirmationSig = pretty "third-party-confirmation"- pretty (OtherSig o) = pretty "unknown signature type" <+> pretty o--$(ATH.deriveJSON ATH.defaultOptions ''SigType)--newtype MPI =- MPI- { unMPI :: Integer- }- deriving (Data, Eq, Generic, Show, Typeable)--instance Wrapped MPI--instance Ord MPI where- compare (MPI a) (MPI b) = compare a b--instance Hashable MPI--instance Pretty MPI where- pretty = pretty . op MPI--$(ATH.deriveJSON ATH.defaultOptions ''MPI)--newtype SignatureSalt =- SignatureSalt- { unSignatureSalt :: ByteString- }- deriving (Data, Eq, Generic, Show, Typeable)--instance Ord SignatureSalt where- compare (SignatureSalt a) (SignatureSalt b) = compare a b--instance Hashable SignatureSalt--instance Pretty SignatureSalt where- pretty (SignatureSalt bs) = prettyLBS bs--instance A.ToJSON SignatureSalt where- toJSON (SignatureSalt bs) = A.toJSON (BL.unpack bs)--data SignaturePayloadVersion- = SigPayloadV3- | SigPayloadV4- | SigPayloadV6- | SigPayloadVOther- deriving (Data, Eq, Generic, Ord, Show, Typeable)--instance Hashable SignaturePayloadVersion--data SignaturePayloadV (v :: SignaturePayloadVersion) where- SigPayloadV3Data ::- SigType- -> ThirtyTwoBitTimeStamp- -> EightOctetKeyId- -> PubKeyAlgorithm- -> HashAlgorithm- -> Word16- -> NonEmpty MPI- -> SignaturePayloadV 'SigPayloadV3- SigPayloadV4Data ::- SigType- -> PubKeyAlgorithm- -> HashAlgorithm- -> [SigSubPacket]- -> [SigSubPacket]- -> Word16- -> NonEmpty MPI- -> SignaturePayloadV 'SigPayloadV4- SigPayloadV6Data ::- SigType- -> PubKeyAlgorithm- -> HashAlgorithm- -> SignatureSalt- -> [SigSubPacket]- -> [SigSubPacket]- -> Word16- -> NonEmpty MPI- -> SignaturePayloadV 'SigPayloadV6- SigPayloadOtherData ::- Word8- -> ByteString- -> SignaturePayloadV 'SigPayloadVOther--deriving instance Eq (SignaturePayloadV v)-deriving instance Show (SignaturePayloadV v)--data SomeSignaturePayload where- SomeSignaturePayload :: SignaturePayloadV v -> SomeSignaturePayload--toSignaturePayload :: SignaturePayloadV v -> SignaturePayload-toSignaturePayload (SigPayloadV3Data st ts eoki pka ha w16 mpis) =- SigV3 st ts eoki pka ha w16 mpis-toSignaturePayload (SigPayloadV4Data st pka ha hsps usps w16 mpis) =- SigV4 st pka ha hsps usps w16 mpis-toSignaturePayload (SigPayloadV6Data st pka ha salt hsps usps w16 mpis) =- SigV6 st pka ha salt hsps usps w16 mpis-toSignaturePayload (SigPayloadOtherData v bs) =- SigVOther v bs--toSomeSignaturePayload :: SignaturePayload -> SomeSignaturePayload-toSomeSignaturePayload (SigV3 st ts eoki pka ha w16 mpis) =- SomeSignaturePayload (SigPayloadV3Data st ts eoki pka ha w16 mpis)-toSomeSignaturePayload (SigV4 st pka ha hsps usps w16 mpis) =- SomeSignaturePayload (SigPayloadV4Data st pka ha hsps usps w16 mpis)-toSomeSignaturePayload (SigV6 st pka ha salt hsps usps w16 mpis) =- SomeSignaturePayload (SigPayloadV6Data st pka ha salt hsps usps w16 mpis)-toSomeSignaturePayload (SigVOther v bs) =- SomeSignaturePayload (SigPayloadOtherData v bs)--signaturePayloadVersion :: SignaturePayload -> SignaturePayloadVersion-signaturePayloadVersion (SigV3 _ _ _ _ _ _ _) = SigPayloadV3-signaturePayloadVersion (SigV4 _ _ _ _ _ _ _) = SigPayloadV4-signaturePayloadVersion (SigV6 _ _ _ _ _ _ _ _) = SigPayloadV6-signaturePayloadVersion (SigVOther _ _) = SigPayloadVOther--asSignaturePayloadV3 :: SignaturePayload -> Either String (SignaturePayloadV 'SigPayloadV3)-asSignaturePayloadV3 (SigV3 st ts eoki pka ha w16 mpis) =- Right (SigPayloadV3Data st ts eoki pka ha w16 mpis)-asSignaturePayloadV3 _ =- Left "Cannot coerce non-v3 SignaturePayload to SignaturePayloadV3"--asSignaturePayloadV4 :: SignaturePayload -> Either String (SignaturePayloadV 'SigPayloadV4)-asSignaturePayloadV4 (SigV4 st pka ha hsps usps w16 mpis) =- Right (SigPayloadV4Data st pka ha hsps usps w16 mpis)-asSignaturePayloadV4 _ =- Left "Cannot coerce non-v4 SignaturePayload to SignaturePayloadV4"--asSignaturePayloadV6 :: SignaturePayload -> Either String (SignaturePayloadV 'SigPayloadV6)-asSignaturePayloadV6 (SigV6 st pka ha salt hsps usps w16 mpis) =- Right (SigPayloadV6Data st pka ha salt hsps usps w16 mpis)-asSignaturePayloadV6 _ =- Left "Cannot coerce non-v6 SignaturePayload to SignaturePayloadV6"--asSignaturePayloadOther :: SignaturePayload -> Either String (SignaturePayloadV 'SigPayloadVOther)-asSignaturePayloadOther (SigVOther v bs) =- Right (SigPayloadOtherData v bs)-asSignaturePayloadOther _ =- Left "Cannot coerce known-version SignaturePayload to SignaturePayloadVOther"--data SignaturePayload- = SigV3- SigType- ThirtyTwoBitTimeStamp- EightOctetKeyId- PubKeyAlgorithm- HashAlgorithm- Word16- (NonEmpty MPI)- | SigV4- SigType- PubKeyAlgorithm- HashAlgorithm- [SigSubPacket]- [SigSubPacket]- Word16- (NonEmpty MPI)- | SigV6- SigType- PubKeyAlgorithm- HashAlgorithm- SignatureSalt- [SigSubPacket]- [SigSubPacket]- Word16- (NonEmpty MPI)- | SigVOther Word8 ByteString- deriving (Data, Eq, Generic, Show, Typeable)--instance Hashable SignaturePayload--instance Ord SignaturePayload where- compare (SigV3 st1 ts1 eoki1 pka1 ha1 w161 mpis1)- (SigV3 st2 ts2 eoki2 pka2 ha2 w162 mpis2) =- compare st1 st2 <> compare ts1 ts2 <> compare eoki1 eoki2 <>- compare pka1 pka2 <> compare ha1 ha2 <> compare w161 w162 <>- compare (NE.toList mpis1) (NE.toList mpis2)- compare (SigV4 st1 pka1 ha1 hsp1 usp1 w161 mpis1)- (SigV4 st2 pka2 ha2 hsp2 usp2 w162 mpis2) =- compare st1 st2 <> compare pka1 pka2 <> compare ha1 ha2 <>- compare hsp1 hsp2 <> compare usp1 usp2 <> compare w161 w162 <>- compare (NE.toList mpis1) (NE.toList mpis2)- compare (SigV6 st1 pka1 ha1 salt1 hsp1 usp1 w161 mpis1)- (SigV6 st2 pka2 ha2 salt2 hsp2 usp2 w162 mpis2) =- compare st1 st2 <> compare pka1 pka2 <> compare ha1 ha2 <>- compare salt1 salt2 <> compare hsp1 hsp2 <> compare usp1 usp2 <>- compare w161 w162 <> compare (NE.toList mpis1) (NE.toList mpis2)- compare (SigVOther t1 bs1) (SigVOther t2 bs2) = compare t1 t2 <> compare bs1 bs2- compare SigV3 {} SigV4 {} = LT- compare SigV3 {} SigV6 {} = LT- compare SigV3 {} SigVOther {} = LT- compare SigV4 {} SigV3 {} = GT- compare SigV4 {} SigV6 {} = LT- compare SigV4 {} SigVOther {} = LT- compare SigV6 {} SigV3 {} = GT- compare SigV6 {} SigV4 {} = GT- compare SigV6 {} SigVOther {} = LT- compare SigVOther {} SigV3 {} = GT- compare SigVOther {} SigV4 {} = GT- compare SigVOther {} SigV6 {} = GT--instance Pretty SignaturePayload where- pretty (SigV3 st ts eoki pka ha w16 mpis) =- pretty "signature v3" <> pretty ':' <+>- pretty st <+>- pretty ts <+>- pretty eoki <+>- pretty pka <+> pretty ha <+> pretty w16 <+> (pretty . NE.toList) mpis- pretty (SigV4 st pka ha hsps usps w16 mpis) =- pretty "signature v4" <> pretty ':' <+>- pretty st <+>- pretty pka <+>- pretty ha <+>- pretty hsps <+> pretty usps <+> pretty w16 <+> (pretty . NE.toList) mpis- pretty (SigV6 st pka ha salt hsps usps w16 mpis) =- pretty "signature v6" <> pretty ':' <+>- pretty st <+>- pretty pka <+>- pretty ha <+>- pretty salt <+>- pretty hsps <+> pretty usps <+> pretty w16 <+> (pretty . NE.toList) mpis- pretty (SigVOther t bs) =- pretty "unknown signature v" <> pretty t <> pretty ':' <+>- pretty (BL.unpack bs)--instance A.ToJSON SignaturePayload where- toJSON (SigV3 st ts eoki pka ha w16 mpis) =- A.toJSON (st, ts, eoki, pka, ha, w16, NE.toList mpis)- toJSON (SigV4 st pka ha hsps usps w16 mpis) =- A.toJSON (st, pka, ha, hsps, usps, w16, NE.toList mpis)- toJSON (SigV6 st pka ha salt hsps usps w16 mpis) =- A.toJSON (st, pka, ha, salt, hsps, usps, w16, NE.toList mpis)- toJSON (SigVOther t bs) = A.toJSON (t, BL.unpack bs)--data IssuerFingerprintVersion- = IssuerFingerprintV4- | IssuerFingerprintV6- deriving (Data, Eq, Generic, Show, Typeable)--instance Ord IssuerFingerprintVersion where- IssuerFingerprintV4 `compare` IssuerFingerprintV4 = EQ- IssuerFingerprintV4 `compare` IssuerFingerprintV6 = LT- IssuerFingerprintV6 `compare` IssuerFingerprintV4 = GT- IssuerFingerprintV6 `compare` IssuerFingerprintV6 = EQ--instance Hashable IssuerFingerprintVersion--instance Pretty IssuerFingerprintVersion where- pretty IssuerFingerprintV4 = pretty "4"- pretty IssuerFingerprintV6 = pretty "6"--instance A.ToJSON IssuerFingerprintVersion where- toJSON IssuerFingerprintV4 = A.toJSON (4 :: Word8)- toJSON IssuerFingerprintV6 = A.toJSON (6 :: Word8)--instance A.FromJSON IssuerFingerprintVersion where- parseJSON (A.Number n) = case round n of- 4 -> pure IssuerFingerprintV4- 6 -> pure IssuerFingerprintV6- _ -> mzero- parseJSON _ = mzero--issuerFingerprintVersionToPacketVersion :: IssuerFingerprintVersion -> PacketVersion-issuerFingerprintVersionToPacketVersion IssuerFingerprintV4 = 4-issuerFingerprintVersionToPacketVersion IssuerFingerprintV6 = 6--packetVersionToIssuerFingerprintVersion :: PacketVersion -> Maybe IssuerFingerprintVersion-packetVersionToIssuerFingerprintVersion 4 = Just IssuerFingerprintV4-packetVersionToIssuerFingerprintVersion 6 = Just IssuerFingerprintV6-packetVersionToIssuerFingerprintVersion _ = Nothing--data SigSubPacketPayload- = SigCreationTime ThirtyTwoBitTimeStamp- | SigExpirationTime ThirtyTwoBitDuration- | ExportableCertification Exportability- | TrustSignature TrustLevel TrustAmount- | RegularExpression AlmostPublicDomainRegex- | Revocable Revocability- | KeyExpirationTime ThirtyTwoBitDuration- | PreferredSymmetricAlgorithms [SymmetricAlgorithm]- | RevocationKey (Set RevocationClass) PubKeyAlgorithm Fingerprint- | Issuer EightOctetKeyId- | NotationData (Set NotationFlag) NotationName NotationValue- | PreferredHashAlgorithms [HashAlgorithm]- | PreferredCompressionAlgorithms [CompressionAlgorithm]- | KeyServerPreferences (Set KSPFlag)- | PreferredKeyServer KeyServer- | PrimaryUserId Bool- | PolicyURL URL- | KeyFlags (Set KeyFlag)- | SignersUserId Text- | ReasonForRevocation RevocationCode RevocationReason- | Features (Set FeatureFlag)- | SignatureTarget PubKeyAlgorithm HashAlgorithm SignatureHash- | EmbeddedSignature SignaturePayload- | IssuerFingerprint IssuerFingerprintVersion Fingerprint- | UserDefinedSigSub Word8 ByteString- | OtherSigSub Word8 ByteString- deriving (Data, Eq, Generic, Ord, Show, Typeable)--instance Hashable SigSubPacketPayload--instance Pretty SigSubPacketPayload where- pretty (SigCreationTime ts) = pretty "creation-time" <+> pretty ts- pretty (SigExpirationTime d) = pretty "sig expiration time" <+> pretty d- pretty (ExportableCertification e) =- pretty "exportable certification" <+> pretty e- pretty (TrustSignature tl ta) =- pretty "trust signature" <+> pretty tl <+> pretty ta- pretty (RegularExpression apdre) =- pretty "regular expression" <+> prettyLBS apdre- pretty (Revocable r) = pretty "revocable" <+> pretty r- pretty (KeyExpirationTime d) = pretty "key expiration time" <+> pretty d- pretty (PreferredSymmetricAlgorithms sas) =- pretty "preferred symmetric algorithms" <+> pretty sas- pretty (RevocationKey rcs pka tof) =- pretty "revocation key" <+>- pretty (Set.toList rcs) <+> pretty pka <+> pretty tof- pretty (Issuer eoki) = pretty "issuer" <+> pretty eoki- pretty (NotationData nfs nn nv) =- pretty "notation data" <+>- pretty (Set.toList nfs) <+> pretty nn <+> pretty nv- pretty (PreferredHashAlgorithms phas) =- pretty "preferred hash algorithms" <+> pretty phas- pretty (PreferredCompressionAlgorithms pcas) =- pretty "preferred compression algorithms" <+> pretty pcas- pretty (KeyServerPreferences kspfs) =- pretty "keyserver preferences" <+> pretty (Set.toList kspfs)- pretty (PreferredKeyServer ks) = pretty "preferred keyserver" <+> prettyLBS ks- pretty (PrimaryUserId p) =- (if p- then mempty- else pretty "NOT ") <>- pretty "primary user-ID"- pretty (PolicyURL u) = pretty "policy URL" <+> pretty u- pretty (KeyFlags kfs) = pretty "key flags" <+> pretty (Set.toList kfs)- pretty (SignersUserId u) = pretty "signer's user-ID" <+> pretty u- pretty (ReasonForRevocation rc rr) =- pretty "reason for revocation" <+> pretty rc <+> pretty rr- pretty (Features ffs) = pretty "features" <+> pretty (Set.toList ffs)- pretty (SignatureTarget pka ha sh) =- pretty "signature target" <+> pretty pka <+> pretty ha <+> prettyLBS sh- pretty (EmbeddedSignature sp) = pretty "embedded signature" <+> pretty sp- pretty (IssuerFingerprint kv ifp) =- pretty "issuer fingerprint (v" <> pretty kv <> pretty ")" <+> pretty ifp- pretty (UserDefinedSigSub t bs) =- pretty "user-defined signature subpacket type" <+>- pretty t <+> pretty (BL.unpack bs)- pretty (OtherSigSub t bs) =- pretty "unknown signature subpacket type" <+> pretty t <+> prettyLBS bs--instance A.ToJSON SigSubPacketPayload where- toJSON (SigCreationTime ts) = object [key "sigCreationTime" .= ts]- toJSON (SigExpirationTime d) = object [key "sigExpirationTime" .= d]- toJSON (ExportableCertification e) =- object [key "exportableCertification" .= e]- toJSON (TrustSignature tl ta) = object [key "trustSignature" .= (tl, ta)]- toJSON (RegularExpression apdre) =- object [key "regularExpression" .= BL.unpack apdre]- toJSON (Revocable r) = object [key "revocable" .= r]- toJSON (KeyExpirationTime d) = object [key "keyExpirationTime" .= d]- toJSON (PreferredSymmetricAlgorithms sas) =- object [key "preferredSymmetricAlgorithms" .= sas]- toJSON (RevocationKey rcs pka tof) =- object [key "revocationKey" .= (rcs, pka, tof)]- toJSON (Issuer eoki) = object [key "issuer" .= eoki]- toJSON (NotationData nfs (NotationName nn) (NotationValue nv)) =- object [key "notationData" .= (nfs, BL.unpack nn, BL.unpack nv)]- toJSON (PreferredHashAlgorithms phas) =- object [key "preferredHashAlgorithms" .= phas]- toJSON (PreferredCompressionAlgorithms pcas) =- object [key "preferredCompressionAlgorithms" .= pcas]- toJSON (KeyServerPreferences kspfs) =- object [key "keyServerPreferences" .= kspfs]- toJSON (PreferredKeyServer ks) =- object [key "preferredKeyServer" .= show ks]- toJSON (PrimaryUserId p) = object [key "primaryUserId" .= p]- toJSON (PolicyURL u) = object [key "policyURL" .= u]- toJSON (KeyFlags kfs) = object [key "keyFlags" .= kfs]- toJSON (SignersUserId u) = object [key "signersUserId" .= u]- toJSON (ReasonForRevocation rc rr) =- object [key "reasonForRevocation" .= (rc, rr)]- toJSON (Features ffs) = object [key "features" .= ffs]- toJSON (SignatureTarget pka ha sh) =- object [key "signatureTarget" .= (pka, ha, BL.unpack sh)]- toJSON (EmbeddedSignature sp) = object [key "embeddedSignature" .= sp]- toJSON (IssuerFingerprint kv ifp) =- object [key "issuerFingerprint" .= (kv, ifp)]- toJSON (UserDefinedSigSub t bs) =- object [key "userDefinedSigSub" .= (t, BL.unpack bs)]- toJSON (OtherSigSub t bs) = object [key "otherSigSub" .= (t, BL.unpack bs)]--uc3 :: (a -> b -> c -> d) -> (a, b, c) -> d-uc3 f ~(a, b, c) = f a b c--instance A.FromJSON SigSubPacketPayload where- parseJSON (A.Object v) =- (SigCreationTime <$> v A..: key "sigCreationTime") <|>- (SigExpirationTime <$> v A..: key "sigExpirationTime") <|>- (ExportableCertification <$> v A..: key "exportableCertification") <|>- (uncurry TrustSignature <$> v A..: key "trustSignature") <|>- (RegularExpression . BL.pack <$> v A..: key "regularExpression") <|>- (Revocable <$> v A..: key "revocable") <|>- (KeyExpirationTime <$> v A..: key "keyExpirationTime") <|>- (PreferredSymmetricAlgorithms <$>- v A..: key "preferredSymmetricAlgorithms") <|>- (uc3 RevocationKey <$> v A..: key "revocationKey") <|>- (Issuer <$> v A..: key "issuer") <|>- (uc3 NotationData <$> v A..: key "notationData")- parseJSON _ = mzero--data SigSubPacket =- SigSubPacket- { _sspCriticality :: Bool- , _sspPayload :: SigSubPacketPayload- }- deriving (Data, Eq, Generic, Show, Typeable)--instance Ord SigSubPacket where- compare (SigSubPacket crit1 payload1) (SigSubPacket crit2 payload2) =- compare crit1 crit2 <> compare payload1 payload2--instance Pretty SigSubPacket where- pretty x =- (if _sspCriticality x- then pretty '*'- else mempty) <>- (pretty . _sspPayload) x--instance Hashable SigSubPacket--instance A.ToJSON SigSubPacket-instance A.FromJSON SigSubPacket--$(makeLenses ''SigSubPacket)---- | Type-safe subpacket list with phantom types to distinguish hashed vs unhashed--- and signature version constraints (v4 vs v6).--- The type parameters erase at runtime; they exist purely for compile-time safety.-newtype SubpacketList (hashedness :: Type) (version :: Type) = SubpacketList [SigSubPacket]- deriving (Show, Eq, Ord, Generic, Data, Typeable)--instance Functor (SubpacketList h) where- fmap _ (SubpacketList sps) = SubpacketList sps---- | Extract the underlying list from a phantom-typed SubpacketList--- This is typically used internally during serialization/deserialization-fromSubpacketList :: SubpacketList h v -> [SigSubPacket]-fromSubpacketList (SubpacketList sps) = sps---- | Wrap a plain list into a phantom-typed SubpacketList--- Warning: This circumvents type safety; use only for trusted sources (e.g., parsing)-toSubpacketList :: [SigSubPacket] -> SubpacketList h v-toSubpacketList = SubpacketList---- | Create an empty hashed subpacket list for a given signature version-emptyHashedSubpackets :: SubpacketList Hashed v-emptyHashedSubpackets = SubpacketList []---- | Create an empty unhashed subpacket list for a given signature version-emptyUnhashedSubpackets :: SubpacketList Unhashed v-emptyUnhashedSubpackets = SubpacketList []---- | Append a subpacket to a hashed list, preserving phantom type-consHashedSubpacket :: SigSubPacket -> SubpacketList Hashed v -> SubpacketList Hashed v-consHashedSubpacket sp (SubpacketList sps) = SubpacketList (sp : sps)---- | Append a subpacket to an unhashed list, preserving phantom type-consUnhashedSubpacket :: SigSubPacket -> SubpacketList Unhashed v -> SubpacketList Unhashed v-consUnhashedSubpacket sp (SubpacketList sps) = SubpacketList (sp : sps)--data KeyVersion- = DeprecatedV3- | V4- | V6- deriving (Data, Eq, Generic, Ord, Show, Typeable)--instance Hashable KeyVersion--instance Pretty KeyVersion where- pretty DeprecatedV3 = pretty "(deprecated) v3"- pretty V4 = pretty "v4"- pretty V6 = pretty "v6"--$(ATH.deriveJSON ATH.defaultOptions ''KeyVersion)--newtype IV =- IV- { unIV :: B.ByteString- }- deriving ( ByteArrayAccess- , Data- , Eq- , Generic- , Hashable- , Semigroup- , Monoid- , Show- , Typeable- )--instance Wrapped IV--instance Ord IV where- compare (IV b1) (IV b2) = compare b1 b2--instance Pretty IV where- pretty = pretty . ("iv:" ++) . bsToHexUpper . BL.fromStrict . op IV--instance A.ToJSON IV where- toJSON = A.toJSON . show . op IV--data DataType- = BinaryData- | TextData- | UTF8Data- | OtherData Word8- deriving (Show, Data, Generic, Typeable)--instance Hashable DataType--instance Eq DataType where- (==) a b = fromFVal a == fromFVal b--instance Ord DataType where- compare = comparing fromFVal--instance FutureVal DataType where- fromFVal BinaryData = fromIntegral . fromEnum $ 'b'- fromFVal TextData = fromIntegral . fromEnum $ 't'- fromFVal UTF8Data = fromIntegral . fromEnum $ 'u'- fromFVal (OtherData o) = o- toFVal 0x62 = BinaryData- toFVal 0x74 = TextData- toFVal 0x75 = UTF8Data- toFVal o = OtherData o--instance Pretty DataType where- pretty BinaryData = pretty "binary"- pretty TextData = pretty "text"- pretty UTF8Data = pretty "UTF-8"- pretty (OtherData o) = pretty "other data type " <+> pretty o--$(ATH.deriveJSON ATH.defaultOptions ''DataType)--newtype SessionKey =- SessionKey- { unSessionKey :: B.ByteString- }- deriving (Data, Eq, Generic, Hashable, Show, Typeable)--instance Wrapped SessionKey--instance Ord SessionKey where- compare (SessionKey b1) (SessionKey b2) = compare b1 b2--newtype Salt =- Salt- { unSalt :: B.ByteString- }- deriving (Data, Eq, Generic, Hashable, Show, Typeable)--instance Wrapped Salt--instance Ord Salt where- compare (Salt b1) (Salt b2) = compare b1 b2--instance Pretty Salt where- pretty = pretty . ("salt:" ++) . bsToHexUpper . BL.fromStrict . op Salt--instance A.ToJSON Salt where- toJSON = A.toJSON . show . op Salt--newtype Salt8 =- Salt8- { unSalt8 :: B.ByteString- }- deriving (Data, Eq, Generic, Hashable, Show, Typeable)--instance Wrapped Salt8--instance Ord Salt8 where- compare (Salt8 b1) (Salt8 b2) = compare b1 b2--instance Pretty Salt8 where- pretty = pretty . ("salt8:" ++) . bsToHexUpper . BL.fromStrict . op Salt8--instance A.ToJSON Salt8 where- toJSON = A.toJSON . show . op Salt8--newtype Salt16 =- Salt16- { unSalt16 :: B.ByteString- }- deriving (Data, Eq, Generic, Hashable, Show, Typeable)--instance Wrapped Salt16--instance Ord Salt16 where- compare (Salt16 b1) (Salt16 b2) = compare b1 b2--instance Pretty Salt16 where- pretty = pretty . ("salt16:" ++) . bsToHexUpper . BL.fromStrict . op Salt16--instance A.ToJSON Salt16 where- toJSON = A.toJSON . show . op Salt16--salt8FromSalt :: Salt -> Maybe Salt8-salt8FromSalt (Salt bs)- | B.length bs == 8 = Just (Salt8 bs)- | otherwise = Nothing--salt16FromSalt :: Salt -> Maybe Salt16-salt16FromSalt (Salt bs)- | B.length bs == 16 = Just (Salt16 bs)- | otherwise = Nothing--saltFromSalt8 :: Salt8 -> Salt-saltFromSalt8 (Salt8 bs) = Salt bs--saltFromSalt16 :: Salt16 -> Salt-saltFromSalt16 (Salt16 bs) = Salt bs--newtype IterationCount =- IterationCount- { unIterationCount :: Int- }- deriving ( Bounded- , Data- , Enum- , Eq- , Generic- , Hashable- , Integral- , Num- , Ord- , Real- , Show- , Typeable- )--instance Wrapped IterationCount--instance Pretty IterationCount where- pretty = pretty . op IterationCount--$(ATH.deriveJSON ATH.defaultOptions ''IterationCount)--data S2K- = Simple HashAlgorithm- | Salted HashAlgorithm Salt8- | IteratedSalted HashAlgorithm Salt8 IterationCount- | Argon2 Salt16 Word8 Word8 Word8- | OtherS2K Word8 ByteString- deriving (Data, Eq, Generic, Show, Typeable)--instance Hashable S2K--instance Ord S2K where- compare (Simple ha1) (Simple ha2) = compare ha1 ha2- compare (Salted ha1 s1) (Salted ha2 s2) = compare ha1 ha2 <> compare s1 s2- compare (IteratedSalted ha1 s1 ic1) (IteratedSalted ha2 s2 ic2) =- compare ha1 ha2 <> compare s1 s2 <> compare ic1 ic2- compare (Argon2 salt1 t1 p1 em1) (Argon2 salt2 t2 p2 em2) =- compare salt1 salt2 <> compare t1 t2 <> compare p1 p2 <> compare em1 em2- compare (OtherS2K t1 bs1) (OtherS2K t2 bs2) = compare t1 t2 <> compare bs1 bs2- compare Simple {} Salted {} = LT- compare Simple {} IteratedSalted {} = LT- compare Simple {} Argon2 {} = LT- compare Simple {} OtherS2K {} = LT- compare Salted {} Simple {} = GT- compare Salted {} IteratedSalted {} = LT- compare Salted {} Argon2 {} = LT- compare Salted {} OtherS2K {} = LT- compare IteratedSalted {} Simple {} = GT- compare IteratedSalted {} Salted {} = GT- compare IteratedSalted {} Argon2 {} = LT- compare IteratedSalted {} OtherS2K {} = LT- compare Argon2 {} Simple {} = GT- compare Argon2 {} Salted {} = GT- compare Argon2 {} IteratedSalted {} = GT- compare Argon2 {} OtherS2K {} = LT- compare OtherS2K {} _ = GT--instance Pretty S2K where- pretty (Simple ha) = pretty "simple S2K," <+> pretty ha- pretty (Salted ha salt) = pretty "salted S2K," <+> pretty ha <+> pretty salt- pretty (IteratedSalted ha salt icount) =- pretty "iterated-salted S2K," <+>- pretty ha <+> pretty salt <+> pretty icount- pretty (Argon2 salt t p em) =- pretty "Argon2 S2K," <+>- pretty salt <+> pretty t <+> pretty p <+> pretty em- pretty (OtherS2K t bs) =- pretty "unknown S2K type" <+> pretty t <+> pretty (bsToHexUpper bs)--instance A.ToJSON S2K where- toJSON (Simple ha) = A.toJSON ha- toJSON (Salted ha salt) = A.toJSON (ha, salt)- toJSON (IteratedSalted ha salt icount) = A.toJSON (ha, salt, icount)- toJSON (Argon2 salt t p em) = A.toJSON (salt, t, p, em)- toJSON (OtherS2K t bs) = A.toJSON (t, BL.unpack bs)--data ImageFormat- = JPEG- | OtherImage Word8- deriving (Data, Generic, Show, Typeable)--instance Eq ImageFormat where- (==) a b = fromFVal a == fromFVal b--instance Ord ImageFormat where- compare = comparing fromFVal--instance FutureVal ImageFormat where- fromFVal JPEG = 1- fromFVal (OtherImage o) = o- toFVal 1 = JPEG- toFVal o = OtherImage o--instance Hashable ImageFormat--instance Pretty ImageFormat where- pretty JPEG = pretty "JPEG"- pretty (OtherImage o) = pretty "unknown image format" <+> pretty o--$(ATH.deriveJSON ATH.defaultOptions ''ImageFormat)--newtype ImageHeader =- ImageHV1 ImageFormat- deriving (Data, Eq, Generic, Show, Typeable)--instance Ord ImageHeader where- compare (ImageHV1 a) (ImageHV1 b) = compare a b--instance Hashable ImageHeader--instance Pretty ImageHeader where- pretty (ImageHV1 f) = pretty "imghdr v1" <+> pretty f--$(ATH.deriveJSON ATH.defaultOptions ''ImageHeader)--data UserAttrSubPacket- = ImageAttribute ImageHeader ImageData- | OtherUASub Word8 ByteString- deriving (Data, Eq, Generic, Show, Typeable)--instance Hashable UserAttrSubPacket--instance Ord UserAttrSubPacket where- compare (ImageAttribute h1 d1) (ImageAttribute h2 d2) =- compare h1 h2 <> compare d1 d2- compare (ImageAttribute _ _) (OtherUASub _ _) = LT- compare (OtherUASub _ _) (ImageAttribute _ _) = GT- compare (OtherUASub t1 b1) (OtherUASub t2 b2) = compare t1 t2 <> compare b1 b2--instance Pretty UserAttrSubPacket where- pretty (ImageAttribute ih d) =- pretty "image-attribute" <+> pretty ih <+> pretty (BL.unpack d)- pretty (OtherUASub t bs) =- pretty "unknown attribute type" <> pretty t <+> pretty (BL.unpack bs)--instance A.ToJSON UserAttrSubPacket where- toJSON (ImageAttribute ih d) = A.toJSON (ih, BL.unpack d)- toJSON (OtherUASub t bs) = A.toJSON (t, BL.unpack bs)--data ECCCurve- = NISTP256- | NISTP384- | NISTP521- | Curve25519- | Curve448- deriving (Data, Eq, Generic, Ord, Show, Typeable)--instance Pretty ECCCurve where- pretty NISTP256 = pretty "NIST P-256"- pretty NISTP384 = pretty "NIST P-384"- pretty NISTP521 = pretty "NIST P-521"- pretty Curve25519 = pretty "Curve25519"- pretty Curve448 = pretty "Curve448"--instance Hashable ECCCurve---- Packet stream wrapper used to provide an EOF-delimited Binary instance.-newtype Block a =- Block- { unBlock :: [a]- } -- intentionally not encoded as a list length prefix- deriving (Show, Eq)+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}++module Codec.Encryption.OpenPGP.Types.Internal.Base+ ( KeyVersion (..)+ , IV (..)+ , S2K (..)+ , SymmetricAlgorithm (..)+ , AEADAlgorithm (..)+ , PubKeyAlgorithm (..)+ , ThirtyTwoBitTimeStamp (..)+ , ThirtyTwoBitDuration (..)+ , Exportability+ , TrustLevel+ , TrustAmount+ , AlmostPublicDomainRegex+ , Revocability+ , RevocationReason+ , KeyServer+ , SignatureHash+ , PacketVersion+ , V3Expiration+ , CompressedDataPayload+ , FileName+ , ImageData+ , NestedFlag+ , HashAlgorithm (..)+ , bsToHexUpper+ , Hashed+ , Unhashed+ , V4Sig+ , V6Sig+ , ByteRange (..)+ , WireRepSourceId (..)+ , WireRepRef (..)+ , WireRepRefs+ , wireRepRef+ , namedWireRepRef+ , mkWireRepRefWithLength+ , mkWireRepRef+ , SignaturePayload (..)+ , Fingerprint (..)+ , SessionKey (..)+ , SigType (..)+ , SignatureSalt (..)+ , Salt (..)+ , Salt8 (..)+ , Salt16 (..)+ , CompressionAlgorithm (..)+ , LiteralDataType (..)+ , UserAttrSubPacket (..)+ , EightOctetKeyId (..)+ , MPI (..)+ , SignaturePayloadVersion (..)+ , SignaturePayloadV (..)+ , SomeSignaturePayload (..)+ , toSignaturePayload+ , toSomeSignaturePayload+ , signaturePayloadVersion+ , asSignaturePayloadV3+ , asSignaturePayloadV4+ , asSignaturePayloadV6+ , asSignaturePayloadOther+ , FutureVal (..)+ , SigSubPacket (..)+ , SigSubPacketPayload (..)+ , ECCCurve (..)+ , IssuerFingerprintVersion (..)+ , IterationCount (..)+ , salt8FromSalt+ , salt16FromSalt+ , saltFromSalt8+ , saltFromSalt16+ , Block (..)+ , FutureFlag (..)+ , ImageHeader (..)+ , issuerFingerprintVersionToPacketVersion+ , packetVersionToIssuerFingerprintVersion+ , NotationName (..)+ , NotationValue (..)+ , URL (..)+ , RevocationClass (..)+ , SubpacketList (..)+ , fromSubpacketList+ , toSubpacketList+ , emptyHashedSubpackets+ , emptyUnhashedSubpackets+ , consHashedSubpacket+ , consUnhashedSubpacket+ , spanByteRanges+ , RevocationCode (..)+ , KeyFlag (..)+ , KeyIdentifier (..)+ , FeatureFlag (..)+ , NotationFlag (..)+ , KSPFlag (..)+ , mkRevocationClass+ , mkNotationFlag+ , SpacedFingerprint (..)+ , ImageFormat (..)+ ) where++import Control.Applicative ((<|>))+import Control.Arrow ((***))+import Control.Lens (Wrapped, makeLenses, op)+import Control.Monad (mzero)+import Data.Aeson (object, (.=))+import qualified Data.Aeson as A+import qualified Data.Aeson.Key as AK+import qualified Data.Aeson.TH as ATH+import Data.Bits ((.&.))+import Data.ByteArray (ByteArrayAccess)+import qualified Data.ByteString as B+import qualified Data.ByteString.Base16.Lazy as B16L+import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Lazy.Char8 as BLC8+import Data.Char (toLower, toUpper)+import Data.Data (Data)+import Data.Hashable (Hashable (..))+import Data.IORef (IORef, atomicModifyIORef', newIORef)+import Data.Int (Int64)+import Data.Kind (Type)+import Data.List (unfoldr)+import Data.List.NonEmpty (NonEmpty)+import qualified Data.List.NonEmpty as NE+import Data.List.Split (chunksOf)+import Data.Maybe (fromMaybe)+import Data.Ord (comparing)+import Data.Set (Set)+import qualified Data.Set as Set+import Data.Text (Text)+import Data.Time.Clock.POSIX (posixSecondsToUTCTime)+import Data.Time.Format (formatTime)+import Data.Time.Locale.Compat (defaultTimeLocale)+import Data.Typeable (Typeable)+import Data.Word (Word16, Word32, Word8)+import GHC.Generics (Generic)+import Network.URI (URI (..), nullURI, parseURI, uriToString)+import Numeric (readHex)+import Prettyprinter (Pretty (..), hsep, punctuate, space, (<+>))+import System.IO.Unsafe (unsafePerformIO)++import Codec.Encryption.OpenPGP.Types.Internal.PrettyUtils+ ( prettyLBS+ )++type Exportability = Bool++type TrustLevel = Word8++type TrustAmount = Word8++type AlmostPublicDomainRegex = ByteString++type Revocability = Bool++type RevocationReason = Text++type KeyServer = ByteString++type SignatureHash = ByteString++type PacketVersion = Word8++type V3Expiration = Word16++type CompressedDataPayload = ByteString++type FileName = ByteString++type ImageData = ByteString++type NestedFlag = Bool++{- | Phantom types for tracking subpacket classification and signature version+These types are never instantiated; they exist purely for compile-time type safety.+-}++-- | Phantom marker for hashed subpackets (included in signature hash computation)+data Hashed++-- | Phantom marker for unhashed subpackets (not included in signature hash computation)+data Unhashed++-- | Phantom marker for v4 signatures (8-octet issuer, no salt)+data V4Sig++-- | Phantom marker for v6 signatures (fingerprint issuer, requires salt)+data V6Sig++data ByteRange+ = ByteRange+ { _rangeOffset :: Int64+ , _rangeLength :: Int64+ }+ deriving (Data, Eq, Generic, Ord, Show, Typeable)++newtype WireRepSourceId+ = WireRepSourceId+ { _unWireRepSourceId :: Int64+ }+ deriving (Data, Eq, Generic, Ord, Show, Typeable)++data WireRepRef+ = WireRepRef+ { _wireRepSourceId :: WireRepSourceId+ , _wireRepLength :: Int64+ , _wireRepName :: Maybe Text+ , _wireRepWasOriginallyArmored :: Bool+ }+ deriving (Data, Eq, Generic, Ord, Show, Typeable)++type WireRepRefs = NonEmpty WireRepRef++-- FIXME: these functions should be moved into a separate module+wireRepRef :: ByteString -> WireRepRef+wireRepRef = mkWireRepRefWithLength Nothing False . BL.length++namedWireRepRef :: Text -> ByteString -> WireRepRef+namedWireRepRef name = mkWireRepRefWithLength (Just name) False . BL.length++mkWireRepRefWithLength+ :: Maybe Text -> Bool -> Int64 -> WireRepRef+mkWireRepRefWithLength mname wasOriginallyArmored payloadLen =+ WireRepRef+ { _wireRepSourceId = freshWireRepSourceId payloadLen+ , _wireRepLength = payloadLen+ , _wireRepName = mname+ , _wireRepWasOriginallyArmored = wasOriginallyArmored+ }++mkWireRepRef :: Maybe Text -> Bool -> ByteString -> WireRepRef+mkWireRepRef mname wasOriginallyArmored =+ mkWireRepRefWithLength mname wasOriginallyArmored . BL.length++wireRepSourceCounter :: IORef Int64+wireRepSourceCounter = unsafePerformIO (newIORef 0)+{-# NOINLINE wireRepSourceCounter #-}+freshWireRepSourceId :: Int64 -> WireRepSourceId+freshWireRepSourceId !_ =+ unsafePerformIO $+ atomicModifyIORef'+ wireRepSourceCounter+ ( \n ->+ let n' = n + 1+ in (n', WireRepSourceId n')+ )+{-# NOINLINE freshWireRepSourceId #-}+rangeEnd :: ByteRange -> Int64+rangeEnd r = _rangeOffset r + _rangeLength r++spanByteRanges :: [ByteRange] -> Maybe ByteRange+spanByteRanges [] = Nothing+spanByteRanges (r : rs) =+ let start = minimum (_rangeOffset <$> (r : rs))+ ending = maximum (rangeEnd <$> (r : rs))+ in Just (ByteRange start (ending - start))++$(makeLenses ''ByteRange)++$(makeLenses ''WireRepRef)++class+ (Eq a, Ord a) =>+ FutureFlag a+ where+ fromFFlag :: a -> Int+ toFFlag :: Int -> a++class+ (Eq a, Ord a) =>+ FutureVal a+ where+ fromFVal :: a -> Word8+ toFVal :: Word8 -> a++data SymmetricAlgorithm+ = Plaintext+ | IDEA+ | TripleDES+ | CAST5+ | Blowfish+ | ReservedSAFER+ | ReservedDES+ | AES128+ | AES192+ | AES256+ | Twofish+ | Camellia128+ | Camellia192+ | Camellia256+ | OtherSA Word8+ deriving (Data, Generic, Show, Typeable)++instance Eq SymmetricAlgorithm where+ (==) a b = fromFVal a == fromFVal b++instance Ord SymmetricAlgorithm where+ compare = comparing fromFVal++instance FutureVal SymmetricAlgorithm where+ fromFVal Plaintext = 0+ fromFVal IDEA = 1+ fromFVal TripleDES = 2+ fromFVal CAST5 = 3+ fromFVal Blowfish = 4+ fromFVal ReservedSAFER = 5+ fromFVal ReservedDES = 6+ fromFVal AES128 = 7+ fromFVal AES192 = 8+ fromFVal AES256 = 9+ fromFVal Twofish = 10+ fromFVal Camellia128 = 11+ fromFVal Camellia192 = 12+ fromFVal Camellia256 = 13+ fromFVal (OtherSA o) = o+ toFVal 0 = Plaintext+ toFVal 1 = IDEA+ toFVal 2 = TripleDES+ toFVal 3 = CAST5+ toFVal 4 = Blowfish+ toFVal 5 = ReservedSAFER+ toFVal 6 = ReservedDES+ toFVal 7 = AES128+ toFVal 8 = AES192+ toFVal 9 = AES256+ toFVal 10 = Twofish+ toFVal 11 = Camellia128+ toFVal 12 = Camellia192+ toFVal 13 = Camellia256+ toFVal o = OtherSA o++instance Hashable SymmetricAlgorithm++instance Pretty SymmetricAlgorithm where+ pretty Plaintext = pretty "plaintext"+ pretty IDEA = pretty "IDEA"+ pretty TripleDES = pretty "3DES"+ pretty CAST5 = pretty "CAST-128"+ pretty Blowfish = pretty "Blowfish"+ pretty ReservedSAFER = pretty "(reserved) SAFER"+ pretty ReservedDES = pretty "(reserved) DES"+ pretty AES128 = pretty "AES-128"+ pretty AES192 = pretty "AES-192"+ pretty AES256 = pretty "AES-256"+ pretty Twofish = pretty "Twofish"+ pretty Camellia128 = pretty "Camellia-128"+ pretty Camellia192 = pretty "Camellia-192"+ pretty Camellia256 = pretty "Camellia-256"+ pretty (OtherSA sa) = pretty "unknown symmetric algorithm" <+> pretty sa++$(ATH.deriveJSON ATH.defaultOptions ''SymmetricAlgorithm)++data NotationFlag+ = HumanReadable+ | OtherNF Word8+ deriving (Data, Generic, Show, Typeable)++mkNotationFlag :: Word8 -> NotationFlag+mkNotationFlag o+ | o' == 0 = HumanReadable+ | otherwise = OtherNF o'+ where+ o' = o .&. 0x0f++instance Eq NotationFlag where+ (==) a b = fromFFlag a == fromFFlag b++instance Ord NotationFlag where+ compare = comparing fromFFlag++instance FutureFlag NotationFlag where+ fromFFlag HumanReadable = 0+ fromFFlag (OtherNF o) = fromIntegral (o .&. 0x0f)+ toFFlag 0 = HumanReadable+ toFFlag o = mkNotationFlag (fromIntegral o)++instance Hashable NotationFlag++instance Pretty NotationFlag where+ pretty HumanReadable = pretty "human-readable"+ pretty (OtherNF o) = pretty "unknown notation flag type" <+> pretty o++$(ATH.deriveJSON ATH.defaultOptions ''NotationFlag)++newtype ThirtyTwoBitTimeStamp+ = ThirtyTwoBitTimeStamp+ { unThirtyTwoBitTimeStamp :: Word32+ }+ deriving+ ( Bounded+ , Data+ , Enum+ , Eq+ , Generic+ , Hashable+ , Integral+ , Num+ , Ord+ , Real+ , Show+ , Typeable+ )++instance Wrapped ThirtyTwoBitTimeStamp++instance Pretty ThirtyTwoBitTimeStamp where+ pretty =+ pretty+ . formatTime defaultTimeLocale "%Y%m%d-%H%M%S"+ . posixSecondsToUTCTime+ . realToFrac++$(ATH.deriveJSON ATH.defaultOptions ''ThirtyTwoBitTimeStamp)++durU :: (Integral a, Show a) => a -> Maybe (String, a)+durU x+ | x >= 31557600 =+ Just ((++ "y") . show $ x `div` 31557600, x `mod` 31557600)+ | x >= 2629800 =+ Just ((++ "m") . show $ x `div` 2629800, x `mod` 2629800)+ | x >= 86400 =+ Just ((++ "d") . show $ x `div` 86400, x `mod` 86400)+ | x > 0 = Just ((++ "s") . show $ x, 0)+ | otherwise = Nothing++newtype ThirtyTwoBitDuration+ = ThirtyTwoBitDuration+ { unThirtyTwoBitDuration :: Word32+ }+ deriving+ ( Bounded+ , Data+ , Enum+ , Eq+ , Generic+ , Hashable+ , Integral+ , Num+ , Ord+ , Real+ , Show+ , Typeable+ )++instance Wrapped ThirtyTwoBitDuration++instance Pretty ThirtyTwoBitDuration where+ pretty = pretty . concat . unfoldr durU . op ThirtyTwoBitDuration++$(ATH.deriveJSON ATH.defaultOptions ''ThirtyTwoBitDuration)++data RevocationClass+ = SensitiveRK+ | RClOther Word8+ deriving (Data, Generic, Show, Typeable)++mkRevocationClass :: Word8 -> RevocationClass+mkRevocationClass i+ | i' == 1 = SensitiveRK+ | otherwise = RClOther i'+ where+ i' = i .&. 0x07++instance Eq RevocationClass where+ (==) a b = fromFFlag a == fromFFlag b++instance Ord RevocationClass where+ compare = comparing fromFFlag++instance FutureFlag RevocationClass where+ fromFFlag SensitiveRK = 1+ fromFFlag (RClOther i) = fromIntegral (i .&. 0x07)+ toFFlag 1 = SensitiveRK+ toFFlag i = mkRevocationClass (fromIntegral i)++instance Hashable RevocationClass++instance Pretty RevocationClass where+ pretty SensitiveRK = pretty "sensitive"+ pretty (RClOther o) = pretty "unknown revocation class" <+> pretty o++$(ATH.deriveJSON ATH.defaultOptions ''RevocationClass)++data PubKeyAlgorithm+ = RSA+ | DeprecatedRSAEncryptOnly+ | DeprecatedRSASignOnly+ | ElgamalEncryptOnly+ | DSA+ | ECDH+ | ECDSA+ | ForbiddenElgamal+ | DH+ | EdDSA+ | X25519+ | X448+ | Ed25519+ | Ed448+ | MLDSA65Ed25519 -- ID 30: ML-DSA-65+Ed25519+ | MLDSA87Ed448 -- ID 31: ML-DSA-87+Ed448+ | SLHDSASHAKE128s -- ID 32: SLH-DSA-SHAKE-128s+ | SLHDSASHAKE128f -- ID 33: SLH-DSA-SHAKE-128f+ | SLHDSASHAKE256s -- ID 34: SLH-DSA-SHAKE-256s+ | MLKEM768X25519 -- ID 35: ML-KEM-768+X25519+ | MLKEM1024X448 -- ID 36: ML-KEM-1024+X448+ | OtherPKA Word8+ deriving (Data, Generic, Show, Typeable)++instance Eq PubKeyAlgorithm where+ (==) a b = fromFVal a == fromFVal b++instance Ord PubKeyAlgorithm where+ compare = comparing fromFVal++instance FutureVal PubKeyAlgorithm where+ fromFVal RSA = 1+ fromFVal DeprecatedRSAEncryptOnly = 2+ fromFVal DeprecatedRSASignOnly = 3+ fromFVal ElgamalEncryptOnly = 16+ fromFVal DSA = 17+ fromFVal ECDH = 18+ fromFVal ECDSA = 19+ fromFVal ForbiddenElgamal = 20+ fromFVal DH = 21+ fromFVal EdDSA = 22+ fromFVal X25519 = 25+ fromFVal X448 = 26+ fromFVal Ed25519 = 27+ fromFVal Ed448 = 28+ fromFVal MLDSA65Ed25519 = 30+ fromFVal MLDSA87Ed448 = 31+ fromFVal SLHDSASHAKE128s = 32+ fromFVal SLHDSASHAKE128f = 33+ fromFVal SLHDSASHAKE256s = 34+ fromFVal MLKEM768X25519 = 35+ fromFVal MLKEM1024X448 = 36+ fromFVal (OtherPKA o) = o+ toFVal 1 = RSA+ toFVal 2 = DeprecatedRSAEncryptOnly+ toFVal 3 = DeprecatedRSASignOnly+ toFVal 16 = ElgamalEncryptOnly+ toFVal 17 = DSA+ toFVal 18 = ECDH+ toFVal 19 = ECDSA+ toFVal 20 = ForbiddenElgamal+ toFVal 21 = DH+ toFVal 22 = EdDSA+ toFVal 25 = X25519+ toFVal 26 = X448+ toFVal 27 = Ed25519+ toFVal 28 = Ed448+ toFVal 30 = MLDSA65Ed25519+ toFVal 31 = MLDSA87Ed448+ toFVal 32 = SLHDSASHAKE128s+ toFVal 33 = SLHDSASHAKE128f+ toFVal 34 = SLHDSASHAKE256s+ toFVal 35 = MLKEM768X25519+ toFVal 36 = MLKEM1024X448+ toFVal o = OtherPKA o++instance Hashable PubKeyAlgorithm++instance Pretty PubKeyAlgorithm where+ pretty RSA = pretty "RSA"+ pretty DeprecatedRSAEncryptOnly = pretty "(deprecated) RSA encrypt-only"+ pretty DeprecatedRSASignOnly = pretty "(deprecated) RSA sign-only"+ pretty ElgamalEncryptOnly = pretty "Elgamal encrypt-only"+ pretty DSA = pretty "DSA"+ pretty ECDH = pretty "ECDH"+ pretty ECDSA = pretty "ECDSA"+ pretty ForbiddenElgamal = pretty "(forbidden) Elgamal"+ pretty DH = pretty "DH"+ pretty EdDSA = pretty "EdDSA"+ pretty X25519 = pretty "X25519"+ pretty X448 = pretty "X448"+ pretty Ed25519 = pretty "Ed25519"+ pretty Ed448 = pretty "Ed448"+ pretty MLDSA65Ed25519 = pretty "ML-DSA-65+Ed25519"+ pretty MLDSA87Ed448 = pretty "ML-DSA-87+Ed448"+ pretty SLHDSASHAKE128s = pretty "SLH-DSA-SHAKE-128s"+ pretty SLHDSASHAKE128f = pretty "SLH-DSA-SHAKE-128f"+ pretty SLHDSASHAKE256s = pretty "SLH-DSA-SHAKE-256s"+ pretty MLKEM768X25519 = pretty "ML-KEM-768+X25519"+ pretty MLKEM1024X448 = pretty "ML-KEM-1024+X448"+ pretty (OtherPKA pka) = pretty "unknown pubkey algorithm type" <+> pretty pka++$(ATH.deriveJSON ATH.defaultOptions ''PubKeyAlgorithm)++{- | An OpenPGP fingerprint. Length depends on key version:+16 bytes (v3/MD5), 20 bytes (v4/SHA-1), or 32 bytes (v6/SHA-256).+-}+newtype Fingerprint+ = Fingerprint+ { unFingerprint :: ByteString+ }+ deriving (Data, Eq, Generic, Ord, Show, Typeable)++instance Wrapped Fingerprint++instance Read Fingerprint where+ readsPrec _ s =+ let ws = hexToW8s (filter (/= ' ') s)+ in if null ws+ then []+ else [(Fingerprint (BL.pack (map fst ws)), snd (last ws))]++instance Hashable Fingerprint++instance Pretty Fingerprint where+ pretty = pretty . bsToHexUpper . unFingerprint++instance A.ToJSON Fingerprint where+ toJSON e = object [AK.fromString "fpr" .= (A.toJSON . show . pretty) e]++instance A.FromJSON Fingerprint where+ parseJSON (A.Object v) = Fingerprint . read <$> v A..: AK.fromString "fpr"+ parseJSON _ = mzero++newtype SpacedFingerprint+ = SpacedFingerprint+ { unSpacedFingerprint :: Fingerprint+ }+ deriving (Data, Eq, Generic, Ord, Show, Typeable)++instance Wrapped SpacedFingerprint++instance Pretty SpacedFingerprint where+ pretty =+ hsep+ . punctuate space+ . map hsep+ . chunksOf 5+ . map pretty+ . chunksOf 4+ . bsToHexUpper+ . unFingerprint+ . op SpacedFingerprint++bsToHexUpper :: ByteString -> String+bsToHexUpper = map toUpper . BLC8.unpack . B16L.encode++hexToW8s :: ReadS Word8+hexToW8s = concatMap readHex . chunksOf 2 . map toLower++newtype EightOctetKeyId+ = EightOctetKeyId+ { unEOKI :: ByteString+ }+ deriving (Data, Eq, Generic, Ord, Typeable)++instance Wrapped EightOctetKeyId++instance Pretty EightOctetKeyId where+ pretty = pretty . bsToHexUpper . op EightOctetKeyId++instance Show EightOctetKeyId where+ show = bsToHexUpper . op EightOctetKeyId++instance Read EightOctetKeyId where+ readsPrec _ =+ map ((EightOctetKeyId . BL.pack *** concat) . unzip)+ . chunksOf 8+ . hexToW8s++instance Hashable EightOctetKeyId++instance A.ToJSON EightOctetKeyId where+ toJSON e =+ object+ [AK.fromString "eoki" .= (bsToHexUpper . op EightOctetKeyId) e]++instance A.FromJSON EightOctetKeyId where+ parseJSON (A.Object v) =+ EightOctetKeyId . read <$> v A..: AK.fromString "eoki"+ parseJSON _ = mzero++data KeyIdentifier+ = KeyIdentifierWildcard+ | KeyIdentifierEightOctet EightOctetKeyId+ | KeyIdentifierFingerprint Fingerprint+ deriving (Data, Eq, Generic, Ord, Show, Typeable)++instance Pretty KeyIdentifier where+ pretty KeyIdentifierWildcard = pretty "wildcard"+ pretty (KeyIdentifierEightOctet kid) = pretty "key-id" <+> pretty kid+ pretty (KeyIdentifierFingerprint fp) = pretty "fingerprint" <+> pretty fp++instance A.ToJSON KeyIdentifier where+ toJSON KeyIdentifierWildcard =+ object [AK.fromString "wildcard" .= A.Bool True]+ toJSON (KeyIdentifierEightOctet kid) = object [AK.fromString "keyId" .= kid]+ toJSON (KeyIdentifierFingerprint fp) =+ object [AK.fromString "fingerprint" .= fp]++instance A.FromJSON KeyIdentifier where+ parseJSON (A.Object v) =+ ( (v A..: AK.fromString "wildcard") >>= \isWildcard ->+ if isWildcard+ then pure KeyIdentifierWildcard+ else mzero+ )+ <|> (KeyIdentifierEightOctet <$> v A..: AK.fromString "keyId")+ <|> (KeyIdentifierFingerprint <$> v A..: AK.fromString "fingerprint")+ parseJSON _ = mzero++newtype NotationName+ = NotationName+ { unNotationName :: ByteString+ }+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)++instance Pretty NotationName where+ pretty = prettyLBS . unNotationName++instance Wrapped NotationName++instance A.ToJSON NotationName where+ toJSON nn =+ object+ [AK.fromString "notationname" .= show (op NotationName nn)]++instance A.FromJSON NotationName where+ parseJSON (A.Object v) =+ NotationName . read <$> v A..: AK.fromString "notationname"+ parseJSON _ = mzero++newtype NotationValue+ = NotationValue+ { unNotationValue :: ByteString+ }+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)++instance Pretty NotationValue where+ pretty = prettyLBS . unNotationValue++instance Wrapped NotationValue++instance A.ToJSON NotationValue where+ toJSON nv =+ object+ [AK.fromString "notationvalue" .= show (op NotationValue nv)]++instance A.FromJSON NotationValue where+ parseJSON (A.Object v) =+ NotationValue . read <$> v A..: AK.fromString "notationvalue"+ parseJSON _ = mzero++data HashAlgorithm+ = DeprecatedMD5+ | SHA1+ | RIPEMD160+ | SHA256+ | SHA384+ | SHA512+ | SHA224+ | SHA3_256+ | SHA3_512+ | OtherHA Word8+ deriving (Data, Generic, Show, Typeable)++instance Eq HashAlgorithm where+ (==) a b = fromFVal a == fromFVal b++instance Ord HashAlgorithm where+ compare = comparing fromFVal++instance FutureVal HashAlgorithm where+ fromFVal DeprecatedMD5 = 1+ fromFVal SHA1 = 2+ fromFVal RIPEMD160 = 3+ fromFVal SHA256 = 8+ fromFVal SHA384 = 9+ fromFVal SHA512 = 10+ fromFVal SHA224 = 11+ fromFVal SHA3_256 = 12+ fromFVal SHA3_512 = 14+ fromFVal (OtherHA o) = o+ toFVal 1 = DeprecatedMD5+ toFVal 2 = SHA1+ toFVal 3 = RIPEMD160+ toFVal 8 = SHA256+ toFVal 9 = SHA384+ toFVal 10 = SHA512+ toFVal 11 = SHA224+ toFVal 12 = SHA3_256+ toFVal 14 = SHA3_512+ toFVal o = OtherHA o++instance Hashable HashAlgorithm++instance Pretty HashAlgorithm where+ pretty DeprecatedMD5 = pretty "(deprecated) MD5"+ pretty SHA1 = pretty "SHA-1"+ pretty RIPEMD160 = pretty "RIPEMD-160"+ pretty SHA256 = pretty "SHA-256"+ pretty SHA384 = pretty "SHA-384"+ pretty SHA512 = pretty "SHA-512"+ pretty SHA224 = pretty "SHA-224"+ pretty SHA3_256 = pretty "SHA3-256"+ pretty SHA3_512 = pretty "SHA3-512"+ pretty (OtherHA ha) = pretty "unknown hash algorithm type" <+> pretty ha++$(ATH.deriveJSON ATH.defaultOptions ''HashAlgorithm)++data CompressionAlgorithm+ = Uncompressed+ | ZIP+ | ZLIB+ | BZip2+ | OtherCA Word8+ deriving (Data, Generic, Show, Typeable)++instance Eq CompressionAlgorithm where+ (==) a b = fromFVal a == fromFVal b++instance Ord CompressionAlgorithm where+ compare = comparing fromFVal++instance FutureVal CompressionAlgorithm where+ fromFVal Uncompressed = 0+ fromFVal ZIP = 1+ fromFVal ZLIB = 2+ fromFVal BZip2 = 3+ fromFVal (OtherCA o) = o+ toFVal 0 = Uncompressed+ toFVal 1 = ZIP+ toFVal 2 = ZLIB+ toFVal 3 = BZip2+ toFVal o = OtherCA o++instance Hashable CompressionAlgorithm++instance Pretty CompressionAlgorithm where+ pretty Uncompressed = pretty "uncompressed"+ pretty ZIP = pretty "ZIP"+ pretty ZLIB = pretty "zlib"+ pretty BZip2 = pretty "bzip2"+ pretty (OtherCA ca) =+ pretty "unknown compression algorithm type" <+> pretty ca++$(ATH.deriveJSON ATH.defaultOptions ''CompressionAlgorithm)++data AEADAlgorithm+ = EAX+ | OCB+ | GCM+ | OtherAEADAlgo Word8+ deriving (Data, Generic, Show, Typeable)++instance Eq AEADAlgorithm where+ (==) a b = fromFVal a == fromFVal b++instance Ord AEADAlgorithm where+ compare = comparing fromFVal++instance FutureVal AEADAlgorithm where+ fromFVal EAX = 1+ fromFVal OCB = 2+ fromFVal GCM = 3+ fromFVal (OtherAEADAlgo o) = o+ toFVal 1 = EAX+ toFVal 2 = OCB+ toFVal 3 = GCM+ toFVal o = OtherAEADAlgo o++instance Hashable AEADAlgorithm++instance Pretty AEADAlgorithm where+ pretty EAX = pretty "EAX"+ pretty OCB = pretty "OCB"+ pretty GCM = pretty "GCM"+ pretty (OtherAEADAlgo aa) = pretty "unknown AEAD algorithm type" <+> pretty aa++$(ATH.deriveJSON ATH.defaultOptions ''AEADAlgorithm)++data KSPFlag+ = NoModify+ | KSPOther Int+ deriving (Data, Generic, Show, Typeable)++instance Eq KSPFlag where+ (==) a b = fromFFlag a == fromFFlag b++instance Ord KSPFlag where+ compare = comparing fromFFlag++instance FutureFlag KSPFlag where+ fromFFlag NoModify = 0+ fromFFlag (KSPOther i) = fromIntegral i+ toFFlag 0 = NoModify+ toFFlag i = KSPOther (fromIntegral i)++instance Hashable KSPFlag++instance Pretty KSPFlag where+ pretty NoModify = pretty "no-modify"+ pretty (KSPOther o) =+ pretty "unknown keyserver preference flag type" <+> pretty o++$(ATH.deriveJSON ATH.defaultOptions ''KSPFlag)++data KeyFlag+ = GroupKey+ | AuthKey+ | SplitKey+ | EncryptStorageKey+ | EncryptCommunicationsKey+ | SignDataKey+ | CertifyKeysKey+ | KFOther Int+ deriving (Data, Generic, Show, Typeable)++instance Eq KeyFlag where+ (==) a b = fromFFlag a == fromFFlag b++instance Ord KeyFlag where+ compare = comparing fromFFlag++instance FutureFlag KeyFlag where+ fromFFlag GroupKey = 0+ fromFFlag AuthKey = 2+ fromFFlag SplitKey = 3+ fromFFlag EncryptStorageKey = 4+ fromFFlag EncryptCommunicationsKey = 5+ fromFFlag SignDataKey = 6+ fromFFlag CertifyKeysKey = 7+ fromFFlag (KFOther i) = fromIntegral i+ toFFlag 0 = GroupKey+ toFFlag 2 = AuthKey+ toFFlag 3 = SplitKey+ toFFlag 4 = EncryptStorageKey+ toFFlag 5 = EncryptCommunicationsKey+ toFFlag 6 = SignDataKey+ toFFlag 7 = CertifyKeysKey+ toFFlag i = KFOther (fromIntegral i)++instance Hashable KeyFlag++instance Pretty KeyFlag where+ pretty GroupKey = pretty "group"+ pretty AuthKey = pretty "auth"+ pretty SplitKey = pretty "split"+ pretty EncryptStorageKey = pretty "encrypt-storage"+ pretty EncryptCommunicationsKey = pretty "encrypt-communications"+ pretty SignDataKey = pretty "sign-data"+ pretty CertifyKeysKey = pretty "certify-keys"+ pretty (KFOther o) = pretty "unknown key flag type" <+> pretty o++$(ATH.deriveJSON ATH.defaultOptions ''KeyFlag)++data RevocationCode+ = NoReason+ | KeySuperseded+ | KeyMaterialCompromised+ | KeyRetiredAndNoLongerUsed+ | UserIdInfoNoLongerValid+ | RCoOther Word8+ deriving (Data, Generic, Show, Typeable)++instance Eq RevocationCode where+ (==) a b = fromFVal a == fromFVal b++instance Ord RevocationCode where+ compare = comparing fromFVal++instance FutureVal RevocationCode where+ fromFVal NoReason = 0+ fromFVal KeySuperseded = 1+ fromFVal KeyMaterialCompromised = 2+ fromFVal KeyRetiredAndNoLongerUsed = 3+ fromFVal UserIdInfoNoLongerValid = 32+ fromFVal (RCoOther o) = o+ toFVal 0 = NoReason+ toFVal 1 = KeySuperseded+ toFVal 2 = KeyMaterialCompromised+ toFVal 3 = KeyRetiredAndNoLongerUsed+ toFVal 32 = UserIdInfoNoLongerValid+ toFVal o = RCoOther o++instance Hashable RevocationCode++instance Pretty RevocationCode where+ pretty NoReason = pretty "no reason"+ pretty KeySuperseded = pretty "key superseded"+ pretty KeyMaterialCompromised = pretty "key material compromised"+ pretty KeyRetiredAndNoLongerUsed = pretty "key retired and no longer used"+ pretty UserIdInfoNoLongerValid = pretty "user-ID info no longer valid"+ pretty (RCoOther o) = pretty "unknown revocation code" <+> pretty o++$(ATH.deriveJSON ATH.defaultOptions ''RevocationCode)++data FeatureFlag+ = FeatureSEIPDv1+ | FeatureSEIPDv2+ | FeatureOther Int+ deriving (Data, Generic, Show, Typeable)++instance Eq FeatureFlag where+ (==) a b = fromFFlag a == fromFFlag b++instance Ord FeatureFlag where+ compare = comparing fromFFlag++instance FutureFlag FeatureFlag where+ fromFFlag FeatureSEIPDv1 = 7+ fromFFlag FeatureSEIPDv2 = 4+ fromFFlag (FeatureOther i) = fromIntegral i+ toFFlag 7 = FeatureSEIPDv1+ toFFlag 4 = FeatureSEIPDv2+ toFFlag i = FeatureOther (fromIntegral i)++instance Hashable FeatureFlag++instance Pretty FeatureFlag where+ pretty FeatureSEIPDv1 = pretty "seipd-v1"+ pretty FeatureSEIPDv2 = pretty "seipd-v2"+ pretty (FeatureOther o) = pretty "unknown feature flag type" <+> pretty o++$(ATH.deriveJSON ATH.defaultOptions ''FeatureFlag)++newtype URL+ = URL+ { unURL :: URI+ }+ deriving (Data, Eq, Generic, Ord, Show, Typeable)++instance Wrapped URL++instance Hashable URL where+ hashWithSalt salt (URL (URI s a p q f)) =+ salt+ `hashWithSalt` s+ `hashWithSalt` show a+ `hashWithSalt` p+ `hashWithSalt` q+ `hashWithSalt` f++instance Pretty URL where+ pretty = pretty . (\uri -> uriToString id uri "") . op URL++instance A.ToJSON URL where+ toJSON u =+ object+ [ AK.fromString "uri" .= (\uri -> uriToString id uri "") (op URL u)+ ]++instance A.FromJSON URL where+ parseJSON (A.Object v) =+ URL . fromMaybe nullURI . parseURI <$> v A..: AK.fromString "uri"+ parseJSON _ = mzero++data SigType+ = BinarySig+ | CanonicalTextSig+ | StandaloneSig+ | GenericCert+ | PersonaCert+ | CasualCert+ | PositiveCert+ | SubkeyBindingSig+ | PrimaryKeyBindingSig+ | SignatureDirectlyOnAKey+ | KeyRevocationSig+ | SubkeyRevocationSig+ | CertRevocationSig+ | TimestampSig+ | ThirdPartyConfirmationSig+ | OtherSig Word8+ deriving (Data, Generic, Show, Typeable)++instance Eq SigType where+ (==) a b = fromFVal a == fromFVal b++instance Ord SigType where+ compare = comparing fromFVal++instance FutureVal SigType where+ fromFVal BinarySig = 0x00+ fromFVal CanonicalTextSig = 0x01+ fromFVal StandaloneSig = 0x02+ fromFVal GenericCert = 0x10+ fromFVal PersonaCert = 0x11+ fromFVal CasualCert = 0x12+ fromFVal PositiveCert = 0x13+ fromFVal SubkeyBindingSig = 0x18+ fromFVal PrimaryKeyBindingSig = 0x19+ fromFVal SignatureDirectlyOnAKey = 0x1F+ fromFVal KeyRevocationSig = 0x20+ fromFVal SubkeyRevocationSig = 0x28+ fromFVal CertRevocationSig = 0x30+ fromFVal TimestampSig = 0x40+ fromFVal ThirdPartyConfirmationSig = 0x50+ fromFVal (OtherSig o) = o+ toFVal 0x00 = BinarySig+ toFVal 0x01 = CanonicalTextSig+ toFVal 0x02 = StandaloneSig+ toFVal 0x10 = GenericCert+ toFVal 0x11 = PersonaCert+ toFVal 0x12 = CasualCert+ toFVal 0x13 = PositiveCert+ toFVal 0x18 = SubkeyBindingSig+ toFVal 0x19 = PrimaryKeyBindingSig+ toFVal 0x1F = SignatureDirectlyOnAKey+ toFVal 0x20 = KeyRevocationSig+ toFVal 0x28 = SubkeyRevocationSig+ toFVal 0x30 = CertRevocationSig+ toFVal 0x40 = TimestampSig+ toFVal 0x50 = ThirdPartyConfirmationSig+ toFVal o = OtherSig o++instance Hashable SigType++instance Pretty SigType where+ pretty BinarySig = pretty "binary"+ pretty CanonicalTextSig = pretty "canonical-pretty"+ pretty StandaloneSig = pretty "standalone"+ pretty GenericCert = pretty "generic"+ pretty PersonaCert = pretty "persona"+ pretty CasualCert = pretty "casual"+ pretty PositiveCert = pretty "positive"+ pretty SubkeyBindingSig = pretty "subkey-binding"+ pretty PrimaryKeyBindingSig = pretty "primary-key-binding"+ pretty SignatureDirectlyOnAKey = pretty "signature directly on a key"+ pretty KeyRevocationSig = pretty "key-revocation"+ pretty SubkeyRevocationSig = pretty "subkey-revocation"+ pretty CertRevocationSig = pretty "cert-revocation"+ pretty TimestampSig = pretty "timestamp"+ pretty ThirdPartyConfirmationSig = pretty "third-party-confirmation"+ pretty (OtherSig o) = pretty "unknown signature type" <+> pretty o++$(ATH.deriveJSON ATH.defaultOptions ''SigType)++newtype MPI+ = MPI+ { unMPI :: Integer+ }+ deriving (Data, Eq, Generic, Show, Typeable)++instance Wrapped MPI++instance Ord MPI where+ compare (MPI a) (MPI b) = compare a b++instance Hashable MPI++instance Pretty MPI where+ pretty = pretty . op MPI++$(ATH.deriveJSON ATH.defaultOptions ''MPI)++newtype SignatureSalt+ = SignatureSalt+ { unSignatureSalt :: ByteString+ }+ deriving (Data, Eq, Generic, Show, Typeable)++instance Ord SignatureSalt where+ compare (SignatureSalt a) (SignatureSalt b) = compare a b++instance Hashable SignatureSalt++instance Pretty SignatureSalt where+ pretty (SignatureSalt bs) = prettyLBS bs++instance A.ToJSON SignatureSalt where+ toJSON (SignatureSalt bs) = A.toJSON (BL.unpack bs)++data SignaturePayloadVersion+ = SigPayloadV3+ | SigPayloadV4+ | SigPayloadV6+ | SigPayloadVOther+ deriving (Data, Eq, Generic, Ord, Show, Typeable)++instance Hashable SignaturePayloadVersion++data SignaturePayloadV (v :: SignaturePayloadVersion) where+ SigPayloadV3Data+ :: SigType+ -> ThirtyTwoBitTimeStamp+ -> EightOctetKeyId+ -> PubKeyAlgorithm+ -> HashAlgorithm+ -> Word16+ -> NonEmpty MPI+ -> SignaturePayloadV 'SigPayloadV3+ SigPayloadV4Data+ :: SigType+ -> PubKeyAlgorithm+ -> HashAlgorithm+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> Word16+ -> NonEmpty MPI+ -> SignaturePayloadV 'SigPayloadV4+ SigPayloadV6Data+ :: SigType+ -> PubKeyAlgorithm+ -> HashAlgorithm+ -> SignatureSalt+ -> [SigSubPacket]+ -> [SigSubPacket]+ -> Word16+ -> NonEmpty MPI+ -> SignaturePayloadV 'SigPayloadV6+ SigPayloadOtherData+ :: Word8 -> ByteString -> SignaturePayloadV 'SigPayloadVOther++deriving instance Eq (SignaturePayloadV v)++deriving instance Show (SignaturePayloadV v)++data SomeSignaturePayload where+ SomeSignaturePayload+ :: SignaturePayloadV v -> SomeSignaturePayload++-- FIXME: these functions belong in a separate module+toSignaturePayload :: SignaturePayloadV v -> SignaturePayload+toSignaturePayload (SigPayloadV3Data st ts eoki pka ha w16 mpis) =+ SigV3 st ts eoki pka ha w16 mpis+toSignaturePayload (SigPayloadV4Data st pka ha hsps usps w16 mpis) =+ SigV4 st pka ha hsps usps w16 mpis+toSignaturePayload (SigPayloadV6Data st pka ha salt hsps usps w16 mpis) =+ SigV6 st pka ha salt hsps usps w16 mpis+toSignaturePayload (SigPayloadOtherData v bs) = SigVOther v bs++toSomeSignaturePayload+ :: SignaturePayload -> SomeSignaturePayload+toSomeSignaturePayload (SigV3 st ts eoki pka ha w16 mpis) =+ SomeSignaturePayload+ (SigPayloadV3Data st ts eoki pka ha w16 mpis)+toSomeSignaturePayload (SigV4 st pka ha hsps usps w16 mpis) =+ SomeSignaturePayload+ (SigPayloadV4Data st pka ha hsps usps w16 mpis)+toSomeSignaturePayload (SigV6 st pka ha salt hsps usps w16 mpis) =+ SomeSignaturePayload+ (SigPayloadV6Data st pka ha salt hsps usps w16 mpis)+toSomeSignaturePayload (SigVOther v bs) =+ SomeSignaturePayload (SigPayloadOtherData v bs)++signaturePayloadVersion+ :: SignaturePayload -> SignaturePayloadVersion+signaturePayloadVersion (SigV3 _ _ _ _ _ _ _) = SigPayloadV3+signaturePayloadVersion (SigV4 _ _ _ _ _ _ _) = SigPayloadV4+signaturePayloadVersion (SigV6 _ _ _ _ _ _ _ _) = SigPayloadV6+signaturePayloadVersion (SigVOther _ _) = SigPayloadVOther++asSignaturePayloadV3+ :: SignaturePayload+ -> Either String (SignaturePayloadV 'SigPayloadV3)+asSignaturePayloadV3 (SigV3 st ts eoki pka ha w16 mpis) =+ Right (SigPayloadV3Data st ts eoki pka ha w16 mpis)+asSignaturePayloadV3 _ =+ Left+ "Cannot coerce non-v3 SignaturePayload to SignaturePayloadV3"++asSignaturePayloadV4+ :: SignaturePayload+ -> Either String (SignaturePayloadV 'SigPayloadV4)+asSignaturePayloadV4 (SigV4 st pka ha hsps usps w16 mpis) =+ Right (SigPayloadV4Data st pka ha hsps usps w16 mpis)+asSignaturePayloadV4 _ =+ Left+ "Cannot coerce non-v4 SignaturePayload to SignaturePayloadV4"++asSignaturePayloadV6+ :: SignaturePayload+ -> Either String (SignaturePayloadV 'SigPayloadV6)+asSignaturePayloadV6 (SigV6 st pka ha salt hsps usps w16 mpis) =+ Right (SigPayloadV6Data st pka ha salt hsps usps w16 mpis)+asSignaturePayloadV6 _ =+ Left+ "Cannot coerce non-v6 SignaturePayload to SignaturePayloadV6"++asSignaturePayloadOther+ :: SignaturePayload+ -> Either String (SignaturePayloadV 'SigPayloadVOther)+asSignaturePayloadOther (SigVOther v bs) = Right (SigPayloadOtherData v bs)+asSignaturePayloadOther _ =+ Left+ "Cannot coerce known-version SignaturePayload to SignaturePayloadVOther"++data SignaturePayload+ = SigV3+ SigType+ ThirtyTwoBitTimeStamp+ EightOctetKeyId+ PubKeyAlgorithm+ HashAlgorithm+ Word16+ (NonEmpty MPI)+ | SigV4+ SigType+ PubKeyAlgorithm+ HashAlgorithm+ [SigSubPacket]+ [SigSubPacket]+ Word16+ (NonEmpty MPI)+ | SigV6+ SigType+ PubKeyAlgorithm+ HashAlgorithm+ SignatureSalt+ [SigSubPacket]+ [SigSubPacket]+ Word16+ (NonEmpty MPI)+ | SigVOther Word8 ByteString+ deriving (Data, Eq, Generic, Show, Typeable)++instance Hashable SignaturePayload++instance Ord SignaturePayload where+ compare (SigV3 st1 ts1 eoki1 pka1 ha1 w161 mpis1) (SigV3 st2 ts2 eoki2 pka2 ha2 w162 mpis2) =+ compare st1 st2+ <> compare ts1 ts2+ <> compare eoki1 eoki2+ <> compare pka1 pka2+ <> compare ha1 ha2+ <> compare w161 w162+ <> compare (NE.toList mpis1) (NE.toList mpis2)+ compare (SigV4 st1 pka1 ha1 hsp1 usp1 w161 mpis1) (SigV4 st2 pka2 ha2 hsp2 usp2 w162 mpis2) =+ compare st1 st2+ <> compare pka1 pka2+ <> compare ha1 ha2+ <> compare hsp1 hsp2+ <> compare usp1 usp2+ <> compare w161 w162+ <> compare (NE.toList mpis1) (NE.toList mpis2)+ compare (SigV6 st1 pka1 ha1 salt1 hsp1 usp1 w161 mpis1) (SigV6 st2 pka2 ha2 salt2 hsp2 usp2 w162 mpis2) =+ compare st1 st2+ <> compare pka1 pka2+ <> compare ha1 ha2+ <> compare salt1 salt2+ <> compare hsp1 hsp2+ <> compare usp1 usp2+ <> compare w161 w162+ <> compare (NE.toList mpis1) (NE.toList mpis2)+ compare (SigVOther t1 bs1) (SigVOther t2 bs2) =+ compare t1 t2 <> compare bs1 bs2+ compare SigV3 {} SigV4 {} = LT+ compare SigV3 {} SigV6 {} = LT+ compare SigV3 {} SigVOther {} = LT+ compare SigV4 {} SigV3 {} = GT+ compare SigV4 {} SigV6 {} = LT+ compare SigV4 {} SigVOther {} = LT+ compare SigV6 {} SigV3 {} = GT+ compare SigV6 {} SigV4 {} = GT+ compare SigV6 {} SigVOther {} = LT+ compare SigVOther {} SigV3 {} = GT+ compare SigVOther {} SigV4 {} = GT+ compare SigVOther {} SigV6 {} = GT++instance Pretty SignaturePayload where+ pretty (SigV3 st ts eoki pka ha w16 mpis) =+ pretty "signature v3"+ <> pretty ':'+ <+> pretty st+ <+> pretty ts+ <+> pretty eoki+ <+> pretty pka+ <+> pretty ha+ <+> pretty w16+ <+> (pretty . NE.toList) mpis+ pretty (SigV4 st pka ha hsps usps w16 mpis) =+ pretty "signature v4"+ <> pretty ':'+ <+> pretty st+ <+> pretty pka+ <+> pretty ha+ <+> pretty hsps+ <+> pretty usps+ <+> pretty w16+ <+> (pretty . NE.toList) mpis+ pretty (SigV6 st pka ha salt hsps usps w16 mpis) =+ pretty "signature v6"+ <> pretty ':'+ <+> pretty st+ <+> pretty pka+ <+> pretty ha+ <+> pretty salt+ <+> pretty hsps+ <+> pretty usps+ <+> pretty w16+ <+> (pretty . NE.toList) mpis+ pretty (SigVOther t bs) =+ pretty "unknown signature v"+ <> pretty t+ <> pretty ':'+ <+> pretty (BL.unpack bs)++instance A.ToJSON SignaturePayload where+ toJSON (SigV3 st ts eoki pka ha w16 mpis) =+ A.toJSON (st, ts, eoki, pka, ha, w16, NE.toList mpis)+ toJSON (SigV4 st pka ha hsps usps w16 mpis) =+ A.toJSON (st, pka, ha, hsps, usps, w16, NE.toList mpis)+ toJSON (SigV6 st pka ha salt hsps usps w16 mpis) =+ A.toJSON (st, pka, ha, salt, hsps, usps, w16, NE.toList mpis)+ toJSON (SigVOther t bs) = A.toJSON (t, BL.unpack bs)++data IssuerFingerprintVersion+ = IssuerFingerprintV4+ | IssuerFingerprintV6+ deriving (Data, Eq, Generic, Show, Typeable)++instance Ord IssuerFingerprintVersion where+ IssuerFingerprintV4 `compare` IssuerFingerprintV4 = EQ+ IssuerFingerprintV4 `compare` IssuerFingerprintV6 = LT+ IssuerFingerprintV6 `compare` IssuerFingerprintV4 = GT+ IssuerFingerprintV6 `compare` IssuerFingerprintV6 = EQ++instance Hashable IssuerFingerprintVersion++instance Pretty IssuerFingerprintVersion where+ pretty IssuerFingerprintV4 = pretty "4"+ pretty IssuerFingerprintV6 = pretty "6"++instance A.ToJSON IssuerFingerprintVersion where+ toJSON IssuerFingerprintV4 = A.toJSON (4 :: Word8)+ toJSON IssuerFingerprintV6 = A.toJSON (6 :: Word8)++instance A.FromJSON IssuerFingerprintVersion where+ parseJSON (A.Number n) =+ case round n of+ 4 -> pure IssuerFingerprintV4+ 6 -> pure IssuerFingerprintV6+ _ -> mzero+ parseJSON _ = mzero++-- FIXME: these functions should be in a separate module+issuerFingerprintVersionToPacketVersion+ :: IssuerFingerprintVersion -> PacketVersion+issuerFingerprintVersionToPacketVersion IssuerFingerprintV4 = 4+issuerFingerprintVersionToPacketVersion IssuerFingerprintV6 = 6++packetVersionToIssuerFingerprintVersion+ :: PacketVersion -> Maybe IssuerFingerprintVersion+packetVersionToIssuerFingerprintVersion 4 = Just IssuerFingerprintV4+packetVersionToIssuerFingerprintVersion 6 = Just IssuerFingerprintV6+packetVersionToIssuerFingerprintVersion _ = Nothing++data SigSubPacketPayload+ = SigCreationTime ThirtyTwoBitTimeStamp+ | SigExpirationTime ThirtyTwoBitDuration+ | ExportableCertification Exportability+ | TrustSignature TrustLevel TrustAmount+ | RegularExpression AlmostPublicDomainRegex+ | Revocable Revocability+ | KeyExpirationTime ThirtyTwoBitDuration+ | PreferredSymmetricAlgorithms [SymmetricAlgorithm]+ | RevocationKey (Set RevocationClass) PubKeyAlgorithm Fingerprint+ | Issuer EightOctetKeyId+ | NotationData (Set NotationFlag) NotationName NotationValue+ | PreferredHashAlgorithms [HashAlgorithm]+ | PreferredCompressionAlgorithms [CompressionAlgorithm]+ | KeyServerPreferences (Set KSPFlag)+ | PreferredKeyServer KeyServer+ | PrimaryUserId Bool+ | PolicyURL URL+ | KeyFlags (Set KeyFlag)+ | SignersUserId Text+ | ReasonForRevocation RevocationCode RevocationReason+ | Features (Set FeatureFlag)+ | SignatureTarget PubKeyAlgorithm HashAlgorithm SignatureHash+ | EmbeddedSignature SignaturePayload+ | IssuerFingerprint IssuerFingerprintVersion Fingerprint+ | IntendedRecipient IssuerFingerprintVersion Fingerprint+ | PreferredAEADCiphersuites [(SymmetricAlgorithm, AEADAlgorithm)]+ | UserDefinedSigSub Word8 ByteString+ | OtherSigSub Word8 ByteString+ deriving (Data, Eq, Generic, Ord, Show, Typeable)++instance Hashable SigSubPacketPayload++instance Pretty SigSubPacketPayload where+ pretty (SigCreationTime ts) = pretty "creation-time" <+> pretty ts+ pretty (SigExpirationTime d) = pretty "sig expiration time" <+> pretty d+ pretty (ExportableCertification e) =+ pretty "exportable certification" <+> pretty e+ pretty (TrustSignature tl ta) =+ pretty "trust signature" <+> pretty tl <+> pretty ta+ pretty (RegularExpression apdre) =+ pretty "regular expression" <+> prettyLBS apdre+ pretty (Revocable r) = pretty "revocable" <+> pretty r+ pretty (KeyExpirationTime d) = pretty "key expiration time" <+> pretty d+ pretty (PreferredSymmetricAlgorithms sas) =+ pretty "preferred symmetric algorithms" <+> pretty sas+ pretty (IntendedRecipient kv fp) =+ pretty "intended recipient (v"+ <> pretty kv+ <> pretty ")"+ <+> pretty fp+ pretty (RevocationKey rcs pka tof) =+ pretty "revocation key"+ <+> pretty (Set.toList rcs)+ <+> pretty pka+ <+> pretty tof+ pretty (Issuer eoki) = pretty "issuer" <+> pretty eoki+ pretty (NotationData nfs nn nv) =+ pretty "notation data"+ <+> pretty (Set.toList nfs)+ <+> pretty nn+ <+> pretty nv+ pretty (PreferredHashAlgorithms phas) =+ pretty "preferred hash algorithms" <+> pretty phas+ pretty (PreferredCompressionAlgorithms pcas) =+ pretty "preferred compression algorithms" <+> pretty pcas+ pretty (KeyServerPreferences kspfs) =+ pretty "keyserver preferences" <+> pretty (Set.toList kspfs)+ pretty (PreferredKeyServer ks) = pretty "preferred keyserver" <+> prettyLBS ks+ pretty (PrimaryUserId p) =+ ( if p+ then mempty+ else pretty "NOT "+ )+ <> pretty "primary user-ID"+ pretty (PolicyURL u) = pretty "policy URL" <+> pretty u+ pretty (KeyFlags kfs) = pretty "key flags" <+> pretty (Set.toList kfs)+ pretty (SignersUserId u) = pretty "signer's user-ID" <+> pretty u+ pretty (ReasonForRevocation rc rr) =+ pretty "reason for revocation" <+> pretty rc <+> pretty rr+ pretty (Features ffs) = pretty "features" <+> pretty (Set.toList ffs)+ pretty (SignatureTarget pka ha sh) =+ pretty "signature target"+ <+> pretty pka+ <+> pretty ha+ <+> prettyLBS sh+ pretty (EmbeddedSignature sp) = pretty "embedded signature" <+> pretty sp+ pretty (IssuerFingerprint kv ifp) =+ pretty "issuer fingerprint (v"+ <> pretty kv+ <> pretty ")"+ <+> pretty ifp+ pretty (PreferredAEADCiphersuites ps) =+ pretty "preferred AEAD ciphersuites"+ <+> pretty ps+ pretty (UserDefinedSigSub t bs) =+ pretty "user-defined signature subpacket type"+ <+> pretty t+ <+> pretty (BL.unpack bs)+ pretty (OtherSigSub t bs) =+ pretty "unknown signature subpacket type"+ <+> pretty t+ <+> prettyLBS bs++instance A.ToJSON SigSubPacketPayload where+ toJSON (SigCreationTime ts) = object [AK.fromString "sigCreationTime" .= ts]+ toJSON (SigExpirationTime d) = object [AK.fromString "sigExpirationTime" .= d]+ toJSON (ExportableCertification e) =+ object [AK.fromString "exportableCertification" .= e]+ toJSON (TrustSignature tl ta) =+ object [AK.fromString "trustSignature" .= (tl, ta)]+ toJSON (RegularExpression apdre) =+ object [AK.fromString "regularExpression" .= BL.unpack apdre]+ toJSON (Revocable r) = object [AK.fromString "revocable" .= r]+ toJSON (KeyExpirationTime d) = object [AK.fromString "keyExpirationTime" .= d]+ toJSON (PreferredSymmetricAlgorithms sas) =+ object [AK.fromString "preferredSymmetricAlgorithms" .= sas]+ toJSON (RevocationKey rcs pka tof) =+ object [AK.fromString "revocationKey" .= (rcs, pka, tof)]+ toJSON (Issuer eoki) = object [AK.fromString "issuer" .= eoki]+ toJSON (NotationData nfs (NotationName nn) (NotationValue nv)) =+ object+ [ AK.fromString "notationData" .= (nfs, BL.unpack nn, BL.unpack nv)+ ]+ toJSON (PreferredHashAlgorithms phas) =+ object [AK.fromString "preferredHashAlgorithms" .= phas]+ toJSON (PreferredCompressionAlgorithms pcas) =+ object [AK.fromString "preferredCompressionAlgorithms" .= pcas]+ toJSON (KeyServerPreferences kspfs) =+ object [AK.fromString "keyServerPreferences" .= kspfs]+ toJSON (PreferredKeyServer ks) =+ object [AK.fromString "preferredKeyServer" .= show ks]+ toJSON (PrimaryUserId p) = object [AK.fromString "primaryUserId" .= p]+ toJSON (PolicyURL u) = object [AK.fromString "policyURL" .= u]+ toJSON (KeyFlags kfs) = object [AK.fromString "keyFlags" .= kfs]+ toJSON (SignersUserId u) = object [AK.fromString "signersUserId" .= u]+ toJSON (ReasonForRevocation rc rr) =+ object [AK.fromString "reasonForRevocation" .= (rc, rr)]+ toJSON (Features ffs) = object [AK.fromString "features" .= ffs]+ toJSON (SignatureTarget pka ha sh) =+ object+ [AK.fromString "signatureTarget" .= (pka, ha, BL.unpack sh)]+ toJSON (EmbeddedSignature sp) =+ object [AK.fromString "embeddedSignature" .= sp]+ toJSON (IssuerFingerprint kv ifp) =+ object [AK.fromString "issuerFingerprint" .= (kv, ifp)]+ toJSON (IntendedRecipient kv ifp) =+ object [AK.fromString "intendedRecipient" .= (kv, ifp)]+ toJSON (PreferredAEADCiphersuites ps) =+ object [AK.fromString "preferredAEADCiphersuites" .= ps]+ toJSON (UserDefinedSigSub t bs) =+ object [AK.fromString "userDefinedSigSub" .= (t, BL.unpack bs)]+ toJSON (OtherSigSub t bs) =+ object [AK.fromString "otherSigSub" .= (t, BL.unpack bs)]++uc3 :: (a -> b -> c -> d) -> (a, b, c) -> d+uc3 f ~(a, b, c) = f a b c++instance A.FromJSON SigSubPacketPayload where+ parseJSON (A.Object v) =+ (SigCreationTime <$> v A..: AK.fromString "sigCreationTime")+ <|> (SigExpirationTime <$> v A..: AK.fromString "sigExpirationTime")+ <|> ( ExportableCertification+ <$> v A..: AK.fromString "exportableCertification"+ )+ <|> (uncurry TrustSignature <$> v A..: AK.fromString "trustSignature")+ <|> ( RegularExpression . BL.pack+ <$> v A..: AK.fromString "regularExpression"+ )+ <|> (Revocable <$> v A..: AK.fromString "revocable")+ <|> (KeyExpirationTime <$> v A..: AK.fromString "keyExpirationTime")+ <|> ( PreferredSymmetricAlgorithms+ <$> v A..: AK.fromString "preferredSymmetricAlgorithms"+ )+ <|> (uc3 RevocationKey <$> v A..: AK.fromString "revocationKey")+ <|> (Issuer <$> v A..: AK.fromString "issuer")+ <|> (uc3 NotationData <$> v A..: AK.fromString "notationData")+ <|> ( uncurry IssuerFingerprint+ <$> v A..: AK.fromString "issuerFingerprint"+ )+ <|> ( uncurry IntendedRecipient+ <$> v A..: AK.fromString "intendedRecipient"+ )+ <|> ( PreferredAEADCiphersuites+ <$> v A..: AK.fromString "preferredAEADCiphersuites"+ )+ parseJSON _ = mzero++data SigSubPacket+ = SigSubPacket+ { _sspCriticality :: Bool+ , _sspPayload :: SigSubPacketPayload+ }+ deriving (Data, Eq, Generic, Show, Typeable)++instance Ord SigSubPacket where+ compare (SigSubPacket crit1 payload1) (SigSubPacket crit2 payload2) =+ compare crit1 crit2 <> compare payload1 payload2++instance Pretty SigSubPacket where+ pretty x =+ ( if _sspCriticality x+ then pretty '*'+ else mempty+ )+ <> (pretty . _sspPayload) x++instance Hashable SigSubPacket++instance A.ToJSON SigSubPacket++instance A.FromJSON SigSubPacket++$(makeLenses ''SigSubPacket)++-- FIXME: the SubpacketList type and associated functions should be moved into a separate module++{- | Type-safe subpacket list with phantom types to distinguish hashed vs unhashed+and signature version constraints (v4 vs v6).+-}+newtype SubpacketList (hashedness :: Type) (version :: Type)+ = SubpacketList [SigSubPacket]+ deriving (Data, Eq, Generic, Ord, Show, Typeable)++instance Functor (SubpacketList h) where+ fmap _ (SubpacketList sps) = SubpacketList sps++-- | Extract the underlying list from a phantom-typed SubpacketList+fromSubpacketList :: SubpacketList h v -> [SigSubPacket]+fromSubpacketList (SubpacketList sps) = sps++-- | Wrap a plain list into a phantom-typed SubpacketList+toSubpacketList :: [SigSubPacket] -> SubpacketList h v+toSubpacketList = SubpacketList++-- | Create an empty hashed subpacket list for a given signature version+emptyHashedSubpackets :: SubpacketList Hashed v+emptyHashedSubpackets = SubpacketList []++-- | Create an empty unhashed subpacket list for a given signature version+emptyUnhashedSubpackets :: SubpacketList Unhashed v+emptyUnhashedSubpackets = SubpacketList []++-- | Append a subpacket to a hashed list, preserving phantom type+consHashedSubpacket+ :: SigSubPacket -> SubpacketList Hashed v -> SubpacketList Hashed v+consHashedSubpacket sp (SubpacketList sps) = SubpacketList (sp : sps)++-- | Append a subpacket to an unhashed list, preserving phantom type+consUnhashedSubpacket+ :: SigSubPacket+ -> SubpacketList Unhashed v+ -> SubpacketList Unhashed v+consUnhashedSubpacket sp (SubpacketList sps) = SubpacketList (sp : sps)++data KeyVersion+ = DeprecatedV3+ | V4+ | V6+ deriving (Data, Eq, Generic, Ord, Show, Typeable)++instance Hashable KeyVersion++instance Pretty KeyVersion where+ pretty DeprecatedV3 = pretty "(deprecated) v3"+ pretty V4 = pretty "v4"+ pretty V6 = pretty "v6"++$(ATH.deriveJSON ATH.defaultOptions ''KeyVersion)++newtype IV+ = IV+ { unIV :: B.ByteString+ }+ deriving+ ( ByteArrayAccess+ , Data+ , Eq+ , Generic+ , Hashable+ , Monoid+ , Semigroup+ , Show+ , Typeable+ )++instance Wrapped IV++instance Ord IV where+ compare (IV b1) (IV b2) = compare b1 b2++instance Pretty IV where+ pretty = pretty . ("iv:" ++) . bsToHexUpper . BL.fromStrict . op IV++instance A.ToJSON IV where+ toJSON = A.toJSON . show . op IV++data LiteralDataType+ = BinaryData+ | TextData+ | UTF8Data+ | OtherData Word8+ deriving (Data, Generic, Show, Typeable)++instance Hashable LiteralDataType++instance Eq LiteralDataType where+ (==) a b = fromFVal a == fromFVal b++instance Ord LiteralDataType where+ compare = comparing fromFVal++instance FutureVal LiteralDataType where+ fromFVal BinaryData = fromIntegral . fromEnum $ 'b'+ fromFVal TextData = fromIntegral . fromEnum $ 't'+ fromFVal UTF8Data = fromIntegral . fromEnum $ 'u'+ fromFVal (OtherData o) = o+ toFVal 0x62 = BinaryData+ toFVal 0x74 = TextData+ toFVal 0x75 = UTF8Data+ toFVal o = OtherData o++instance Pretty LiteralDataType where+ pretty BinaryData = pretty "binary"+ pretty TextData = pretty "text"+ pretty UTF8Data = pretty "UTF-8"+ pretty (OtherData o) = pretty "other data type " <+> pretty o++$(ATH.deriveJSON ATH.defaultOptions ''LiteralDataType)++newtype SessionKey+ = SessionKey+ { unSessionKey :: B.ByteString+ }+ deriving (Data, Eq, Generic, Hashable, Show, Typeable)++instance Wrapped SessionKey++instance Ord SessionKey where+ compare (SessionKey b1) (SessionKey b2) = compare b1 b2++newtype Salt+ = Salt+ { unSalt :: B.ByteString+ }+ deriving (Data, Eq, Generic, Hashable, Show, Typeable)++instance Wrapped Salt++instance Ord Salt where+ compare (Salt b1) (Salt b2) = compare b1 b2++instance Pretty Salt where+ pretty = pretty . ("salt:" ++) . bsToHexUpper . BL.fromStrict . op Salt++instance A.ToJSON Salt where+ toJSON = A.toJSON . show . op Salt++newtype Salt8+ = Salt8+ { unSalt8 :: B.ByteString+ }+ deriving (Data, Eq, Generic, Hashable, Show, Typeable)++instance Wrapped Salt8++instance Ord Salt8 where+ compare (Salt8 b1) (Salt8 b2) = compare b1 b2++instance Pretty Salt8 where+ pretty =+ pretty . ("salt8:" ++) . bsToHexUpper . BL.fromStrict . op Salt8++instance A.ToJSON Salt8 where+ toJSON = A.toJSON . show . op Salt8++newtype Salt16+ = Salt16+ { unSalt16 :: B.ByteString+ }+ deriving (Data, Eq, Generic, Hashable, Show, Typeable)++instance Wrapped Salt16++instance Ord Salt16 where+ compare (Salt16 b1) (Salt16 b2) = compare b1 b2++instance Pretty Salt16 where+ pretty =+ pretty+ . ("salt16:" ++)+ . bsToHexUpper+ . BL.fromStrict+ . op Salt16++instance A.ToJSON Salt16 where+ toJSON = A.toJSON . show . op Salt16++-- FIXME: these functions should be in a separate module+salt8FromSalt :: Salt -> Maybe Salt8+salt8FromSalt (Salt bs)+ | B.length bs == 8 = Just (Salt8 bs)+ | otherwise = Nothing++salt16FromSalt :: Salt -> Maybe Salt16+salt16FromSalt (Salt bs)+ | B.length bs == 16 = Just (Salt16 bs)+ | otherwise = Nothing++saltFromSalt8 :: Salt8 -> Salt+saltFromSalt8 (Salt8 bs) = Salt bs++saltFromSalt16 :: Salt16 -> Salt+saltFromSalt16 (Salt16 bs) = Salt bs++newtype IterationCount+ = IterationCount+ { unIterationCount :: Int+ }+ deriving+ ( Bounded+ , Data+ , Enum+ , Eq+ , Generic+ , Hashable+ , Integral+ , Num+ , Ord+ , Real+ , Show+ , Typeable+ )++instance Wrapped IterationCount++instance Pretty IterationCount where+ pretty = pretty . op IterationCount++$(ATH.deriveJSON ATH.defaultOptions ''IterationCount)++data S2K+ = Simple HashAlgorithm+ | Salted HashAlgorithm Salt8+ | IteratedSalted HashAlgorithm Salt8 IterationCount+ | Argon2 Salt16 Word8 Word8 Word8+ | OtherS2K Word8 ByteString+ deriving (Data, Eq, Generic, Show, Typeable)++instance Hashable S2K++instance Ord S2K where+ compare (Simple ha1) (Simple ha2) = compare ha1 ha2+ compare (Salted ha1 s1) (Salted ha2 s2) = compare ha1 ha2 <> compare s1 s2+ compare (IteratedSalted ha1 s1 ic1) (IteratedSalted ha2 s2 ic2) =+ compare ha1 ha2 <> compare s1 s2 <> compare ic1 ic2+ compare (Argon2 salt1 t1 p1 em1) (Argon2 salt2 t2 p2 em2) =+ compare salt1 salt2+ <> compare t1 t2+ <> compare p1 p2+ <> compare em1 em2+ compare (OtherS2K t1 bs1) (OtherS2K t2 bs2) = compare t1 t2 <> compare bs1 bs2+ compare Simple {} Salted {} = LT+ compare Simple {} IteratedSalted {} = LT+ compare Simple {} Argon2 {} = LT+ compare Simple {} OtherS2K {} = LT+ compare Salted {} Simple {} = GT+ compare Salted {} IteratedSalted {} = LT+ compare Salted {} Argon2 {} = LT+ compare Salted {} OtherS2K {} = LT+ compare IteratedSalted {} Simple {} = GT+ compare IteratedSalted {} Salted {} = GT+ compare IteratedSalted {} Argon2 {} = LT+ compare IteratedSalted {} OtherS2K {} = LT+ compare Argon2 {} Simple {} = GT+ compare Argon2 {} Salted {} = GT+ compare Argon2 {} IteratedSalted {} = GT+ compare Argon2 {} OtherS2K {} = LT+ compare OtherS2K {} _ = GT++instance Pretty S2K where+ pretty (Simple ha) = pretty "simple S2K," <+> pretty ha+ pretty (Salted ha salt) = pretty "salted S2K," <+> pretty ha <+> pretty salt+ pretty (IteratedSalted ha salt icount) =+ pretty "iterated-salted S2K,"+ <+> pretty ha+ <+> pretty salt+ <+> pretty icount+ pretty (Argon2 salt t p em) =+ pretty "Argon2 S2K,"+ <+> pretty salt+ <+> pretty t+ <+> pretty p+ <+> pretty em+ pretty (OtherS2K t bs) =+ pretty "unknown S2K type"+ <+> pretty t+ <+> pretty (bsToHexUpper bs)++instance A.ToJSON S2K where+ toJSON (Simple ha) = A.toJSON ha+ toJSON (Salted ha salt) = A.toJSON (ha, salt)+ toJSON (IteratedSalted ha salt icount) = A.toJSON (ha, salt, icount)+ toJSON (Argon2 salt t p em) = A.toJSON (salt, t, p, em)+ toJSON (OtherS2K t bs) = A.toJSON (t, BL.unpack bs)++data ImageFormat+ = JPEG+ | OtherImage Word8+ deriving (Data, Generic, Show, Typeable)++instance Eq ImageFormat where+ (==) a b = fromFVal a == fromFVal b++instance Ord ImageFormat where+ compare = comparing fromFVal++instance FutureVal ImageFormat where+ fromFVal JPEG = 1+ fromFVal (OtherImage o) = o+ toFVal 1 = JPEG+ toFVal o = OtherImage o++instance Hashable ImageFormat++instance Pretty ImageFormat where+ pretty JPEG = pretty "JPEG"+ pretty (OtherImage o) = pretty "unknown image format" <+> pretty o++$(ATH.deriveJSON ATH.defaultOptions ''ImageFormat)++newtype ImageHeader+ = ImageHV1 ImageFormat+ deriving (Data, Eq, Generic, Show, Typeable)++instance Ord ImageHeader where+ compare (ImageHV1 a) (ImageHV1 b) = compare a b++instance Hashable ImageHeader++instance Pretty ImageHeader where+ pretty (ImageHV1 f) = pretty "imghdr v1" <+> pretty f++$(ATH.deriveJSON ATH.defaultOptions ''ImageHeader)++data UserAttrSubPacket+ = ImageAttribute ImageHeader ImageData+ | OtherUASub Word8 ByteString+ deriving (Data, Eq, Generic, Show, Typeable)++instance Hashable UserAttrSubPacket++instance Ord UserAttrSubPacket where+ compare (ImageAttribute h1 d1) (ImageAttribute h2 d2) =+ compare h1 h2 <> compare d1 d2+ compare (ImageAttribute _ _) (OtherUASub _ _) = LT+ compare (OtherUASub _ _) (ImageAttribute _ _) = GT+ compare (OtherUASub t1 b1) (OtherUASub t2 b2) = compare t1 t2 <> compare b1 b2++instance Pretty UserAttrSubPacket where+ pretty (ImageAttribute ih d) =+ pretty "image-attribute" <+> pretty ih <+> pretty (BL.unpack d)+ pretty (OtherUASub t bs) =+ pretty "unknown attribute type"+ <> pretty t+ <+> pretty (BL.unpack bs)++instance A.ToJSON UserAttrSubPacket where+ toJSON (ImageAttribute ih d) = A.toJSON (ih, BL.unpack d)+ toJSON (OtherUASub t bs) = A.toJSON (t, BL.unpack bs)++-- FIXME: should this be merged with EdSigningCurve somehow?+data ECCCurve+ = NISTP256+ | NISTP384+ | NISTP521+ | Curve25519+ | Curve448+ deriving (Data, Eq, Generic, Ord, Show, Typeable)++instance Pretty ECCCurve where+ pretty NISTP256 = pretty "NIST P-256"+ pretty NISTP384 = pretty "NIST P-384"+ pretty NISTP521 = pretty "NIST P-521"+ pretty Curve25519 = pretty "Curve25519"+ pretty Curve448 = pretty "Curve448"++instance Hashable ECCCurve++-- Packet stream wrapper used to provide an EOF-delimited Binary instance.+newtype Block a+ = Block+ { unBlock :: [a]+ } -- intentionally not encoded as a list length prefix+ deriving (Eq, Show)
Codec/Encryption/OpenPGP/Types/Internal/PKITypes.hs view
@@ -2,58 +2,56 @@ -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).- {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE GADTs #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE StandaloneDeriving #-}-{-# LANGUAGE ViewPatterns #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE ViewPatterns #-} module Codec.Encryption.OpenPGP.Types.Internal.PKITypes where -import GHC.Generics (Generic)--import Codec.Encryption.OpenPGP.Types.Internal.Base hiding (Ed25519, Ed448)-import Codec.Encryption.OpenPGP.Types.Internal.CryptonNewtypes- import qualified Data.Aeson as A import qualified Data.ByteString as B import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Lazy as BL-import Data.Data (Data(..))+import Data.Data (Data (..)) import qualified Data.Data as DData-import Data.Hashable (Hashable(..))+import Data.Hashable (Hashable (..)) import Data.Ord (comparing) import Data.Typeable (Typeable) import Data.Word (Word16)-import Prettyprinter (Pretty(..), (<+>))+import GHC.Generics (Generic)+import Prettyprinter (Pretty (..), (<+>)) -data EdSigningCurve =- Ed25519- | Ed448- deriving (Data, Eq, Generic, Ord, Show, Typeable)+import Codec.Encryption.OpenPGP.Types.Internal.Base+import Codec.Encryption.OpenPGP.Types.Internal.CryptonNewtypes +data EdSigningCurve+ = EdSigningCurve25519+ | EdSigningCurve448+ deriving (Data, Eq, Generic, Ord, Show, Typeable)+ instance Hashable EdSigningCurve instance Pretty EdSigningCurve where- pretty Ed25519 = pretty "Ed25519"- pretty Ed448 = pretty "Ed448"+ pretty EdSigningCurve25519 = pretty "Ed25519"+ pretty EdSigningCurve448 = pretty "Ed448" instance A.FromJSON EdSigningCurve instance A.ToJSON EdSigningCurve -newtype EPoint =- EPoint+newtype EPoint+ = EPoint { unEPoint :: Integer }- deriving (Data, Eq, Generic, Ord, Pretty, Show, Typeable)+ deriving (Data, Eq, Generic, Ord, Pretty, Show, Typeable) instance Hashable EPoint @@ -62,263 +60,228 @@ instance A.ToJSON EPoint data EdPoint- = PrefixedNativeEPoint EPoint- | NativeEPoint EPoint- deriving (Data, Eq, Generic, Ord, Show, Typeable)+ = PrefixedNativeEPoint EPoint+ | NativeEPoint EPoint+ deriving (Data, Eq, Generic, Ord, Show, Typeable) instance Hashable EdPoint instance Pretty EdPoint where- pretty (PrefixedNativeEPoint ep) = pretty "prefixed-native" <+> pretty ep- pretty (NativeEPoint ep) = pretty "native" <+> pretty ep+ pretty (PrefixedNativeEPoint ep) = pretty "prefixed-native" <+> pretty ep+ pretty (NativeEPoint ep) = pretty "native" <+> pretty ep instance A.FromJSON EdPoint instance A.ToJSON EdPoint data PKey- = RSAPubKey RSA_PublicKey- | DSAPubKey DSA_PublicKey- | ElGamalPubKey Integer Integer Integer- | ECDHPubKey PKey HashAlgorithm SymmetricAlgorithm- | ECDSAPubKey ECDSA_PublicKey- | EdDSAPubKey EdSigningCurve EdPoint- | UnknownPKey ByteString- deriving (Data, Eq, Generic, Ord, Show, Typeable)+ = RSAPubKey RSA_PublicKey+ | DSAPubKey DSA_PublicKey+ | ElGamalPubKey Integer Integer Integer+ | ECDHPubKey PKey HashAlgorithm SymmetricAlgorithm+ | ECDSAPubKey ECDSA_PublicKey+ | EdDSAPubKey EdSigningCurve EdPoint+ | MLKEMPubKey B.ByteString+ | MLDSAPubKey B.ByteString+ | SLHDSAPubKey B.ByteString+ | UnknownPKey ByteString+ deriving (Data, Eq, Generic, Ord, Show, Typeable) instance Hashable PKey instance Pretty PKey where- pretty (RSAPubKey p) = pretty "RSA" <+> pretty p- pretty (DSAPubKey p) = pretty "DSA" <+> pretty p- pretty (ElGamalPubKey p g y) =- pretty "Elgamal" <+> pretty p <+> pretty g <+> pretty y- pretty (ECDHPubKey p ha sa) =- pretty "ECDH" <+> pretty p <+> pretty ha <+> pretty sa- pretty (ECDSAPubKey p) = pretty "ECDSA" <+> pretty p- pretty (EdDSAPubKey c ep) = pretty c <+> pretty ep- pretty (UnknownPKey bs) = pretty "<unknown>" <+> pretty (bsToHexUpper bs)+ pretty (RSAPubKey p) = pretty "RSA" <+> pretty p+ pretty (DSAPubKey p) = pretty "DSA" <+> pretty p+ pretty (ElGamalPubKey p g y) =+ pretty "Elgamal" <+> pretty p <+> pretty g <+> pretty y+ pretty (ECDHPubKey p ha sa) =+ pretty "ECDH" <+> pretty p <+> pretty ha <+> pretty sa+ pretty (ECDSAPubKey p) = pretty "ECDSA" <+> pretty p+ pretty (EdDSAPubKey c ep) = pretty c <+> pretty ep+ pretty (MLKEMPubKey bs) = pretty "ML-KEM" <+> pretty (bsToHexUpper (BL.fromStrict bs))+ pretty (MLDSAPubKey bs) = pretty "ML-DSA" <+> pretty (bsToHexUpper (BL.fromStrict bs))+ pretty (SLHDSAPubKey bs) = pretty "SLH-DSA" <+> pretty (bsToHexUpper (BL.fromStrict bs))+ pretty (UnknownPKey bs) = pretty "<unknown>" <+> pretty (bsToHexUpper bs) instance A.ToJSON PKey where- toJSON (RSAPubKey p) = A.toJSON p- toJSON (DSAPubKey p) = A.toJSON p- toJSON (ElGamalPubKey p g y) = A.toJSON (p, g, y)- toJSON (ECDHPubKey p ha sa) = A.toJSON (p, ha, sa)- toJSON (ECDSAPubKey p) = A.toJSON p- toJSON (EdDSAPubKey c ep) = A.toJSON (c, ep)- toJSON (UnknownPKey bs) = A.toJSON (BL.unpack bs)+ toJSON (RSAPubKey p) = A.toJSON p+ toJSON (DSAPubKey p) = A.toJSON p+ toJSON (ElGamalPubKey p g y) = A.toJSON (p, g, y)+ toJSON (ECDHPubKey p ha sa) = A.toJSON (p, ha, sa)+ toJSON (ECDSAPubKey p) = A.toJSON p+ toJSON (EdDSAPubKey c ep) = A.toJSON (c, ep)+ toJSON (MLKEMPubKey bs) = A.toJSON (B.unpack bs)+ toJSON (MLDSAPubKey bs) = A.toJSON (B.unpack bs)+ toJSON (SLHDSAPubKey bs) = A.toJSON (B.unpack bs)+ toJSON (UnknownPKey bs) = A.toJSON (BL.unpack bs) data SKey- = RSAPrivateKey RSA_PrivateKey- | DSAPrivateKey DSA_PrivateKey- | ElGamalPrivateKey Integer- | ECDHPrivateKey ECDSA_PrivateKey- | ECDSAPrivateKey ECDSA_PrivateKey- | EdDSAPrivateKey EdSigningCurve B.ByteString- | X25519PrivateKey B.ByteString- | X448PrivateKey B.ByteString- | UnknownSKey ByteString- deriving (Data, Eq, Generic, Show, Typeable)+ = RSAPrivateKey RSA_PrivateKey+ | DSAPrivateKey DSA_PrivateKey+ | ElGamalPrivateKey Integer+ | ECDHPrivateKey ECDSA_PrivateKey+ | ECDSAPrivateKey ECDSA_PrivateKey+ | EdDSAPrivateKey EdSigningCurve B.ByteString+ | X25519PrivateKey B.ByteString+ | X448PrivateKey B.ByteString+ | MLKEMPrivateKey B.ByteString+ | MLDSAPrivateKey B.ByteString+ | SLHDSAPrivateKey B.ByteString+ | UnknownSKey ByteString+ deriving (Data, Eq, Generic, Ord, Show, Typeable) instance Hashable SKey -instance Ord SKey where- compare (RSAPrivateKey rsa1) (RSAPrivateKey rsa2) = compare rsa1 rsa2- compare (DSAPrivateKey dsa1) (DSAPrivateKey dsa2) = compare dsa1 dsa2- compare (ElGamalPrivateKey x1) (ElGamalPrivateKey x2) = compare x1 x2- compare (ECDHPrivateKey ecdsa1) (ECDHPrivateKey ecdsa2) = compare ecdsa1 ecdsa2- compare (ECDSAPrivateKey ecdsa1) (ECDSAPrivateKey ecdsa2) = compare ecdsa1 ecdsa2- compare (EdDSAPrivateKey curve1 bs1) (EdDSAPrivateKey curve2 bs2) =- compare curve1 curve2 <> compare bs1 bs2- compare (X25519PrivateKey bs1) (X25519PrivateKey bs2) = compare bs1 bs2- compare (X448PrivateKey bs1) (X448PrivateKey bs2) = compare bs1 bs2- compare (UnknownSKey bs1) (UnknownSKey bs2) = compare bs1 bs2- compare RSAPrivateKey {} DSAPrivateKey {} = LT- compare RSAPrivateKey {} ElGamalPrivateKey {} = LT- compare RSAPrivateKey {} ECDHPrivateKey {} = LT- compare RSAPrivateKey {} ECDSAPrivateKey {} = LT- compare RSAPrivateKey {} EdDSAPrivateKey {} = LT- compare RSAPrivateKey {} X25519PrivateKey {} = LT- compare RSAPrivateKey {} X448PrivateKey {} = LT- compare RSAPrivateKey {} UnknownSKey {} = LT- compare DSAPrivateKey {} RSAPrivateKey {} = GT- compare DSAPrivateKey {} ElGamalPrivateKey {} = LT- compare DSAPrivateKey {} ECDHPrivateKey {} = LT- compare DSAPrivateKey {} ECDSAPrivateKey {} = LT- compare DSAPrivateKey {} EdDSAPrivateKey {} = LT- compare DSAPrivateKey {} X25519PrivateKey {} = LT- compare DSAPrivateKey {} X448PrivateKey {} = LT- compare DSAPrivateKey {} UnknownSKey {} = LT- compare ElGamalPrivateKey {} RSAPrivateKey {} = GT- compare ElGamalPrivateKey {} DSAPrivateKey {} = GT- compare ElGamalPrivateKey {} ECDHPrivateKey {} = LT- compare ElGamalPrivateKey {} ECDSAPrivateKey {} = LT- compare ElGamalPrivateKey {} EdDSAPrivateKey {} = LT- compare ElGamalPrivateKey {} X25519PrivateKey {} = LT- compare ElGamalPrivateKey {} X448PrivateKey {} = LT- compare ElGamalPrivateKey {} UnknownSKey {} = LT- compare ECDHPrivateKey {} RSAPrivateKey {} = GT- compare ECDHPrivateKey {} DSAPrivateKey {} = GT- compare ECDHPrivateKey {} ElGamalPrivateKey {} = GT- compare ECDHPrivateKey {} ECDSAPrivateKey {} = LT- compare ECDHPrivateKey {} EdDSAPrivateKey {} = LT- compare ECDHPrivateKey {} X25519PrivateKey {} = LT- compare ECDHPrivateKey {} X448PrivateKey {} = LT- compare ECDHPrivateKey {} UnknownSKey {} = LT- compare ECDSAPrivateKey {} RSAPrivateKey {} = GT- compare ECDSAPrivateKey {} DSAPrivateKey {} = GT- compare ECDSAPrivateKey {} ElGamalPrivateKey {} = GT- compare ECDSAPrivateKey {} ECDHPrivateKey {} = GT- compare ECDSAPrivateKey {} EdDSAPrivateKey {} = LT- compare ECDSAPrivateKey {} X25519PrivateKey {} = LT- compare ECDSAPrivateKey {} X448PrivateKey {} = LT- compare ECDSAPrivateKey {} UnknownSKey {} = LT- compare EdDSAPrivateKey {} RSAPrivateKey {} = GT- compare EdDSAPrivateKey {} DSAPrivateKey {} = GT- compare EdDSAPrivateKey {} ElGamalPrivateKey {} = GT- compare EdDSAPrivateKey {} ECDHPrivateKey {} = GT- compare EdDSAPrivateKey {} ECDSAPrivateKey {} = GT- compare EdDSAPrivateKey {} X25519PrivateKey {} = LT- compare EdDSAPrivateKey {} X448PrivateKey {} = LT- compare EdDSAPrivateKey {} UnknownSKey {} = LT- compare X25519PrivateKey {} RSAPrivateKey {} = GT- compare X25519PrivateKey {} DSAPrivateKey {} = GT- compare X25519PrivateKey {} ElGamalPrivateKey {} = GT- compare X25519PrivateKey {} ECDHPrivateKey {} = GT- compare X25519PrivateKey {} ECDSAPrivateKey {} = GT- compare X25519PrivateKey {} EdDSAPrivateKey {} = GT- compare X25519PrivateKey {} X448PrivateKey {} = LT- compare X25519PrivateKey {} UnknownSKey {} = LT- compare X448PrivateKey {} RSAPrivateKey {} = GT- compare X448PrivateKey {} DSAPrivateKey {} = GT- compare X448PrivateKey {} ElGamalPrivateKey {} = GT- compare X448PrivateKey {} ECDHPrivateKey {} = GT- compare X448PrivateKey {} ECDSAPrivateKey {} = GT- compare X448PrivateKey {} EdDSAPrivateKey {} = GT- compare X448PrivateKey {} X25519PrivateKey {} = GT- compare X448PrivateKey {} UnknownSKey {} = LT- compare UnknownSKey {} _ = GT- instance Pretty SKey where- pretty (RSAPrivateKey p) = pretty "RSA" <+> pretty p- pretty (DSAPrivateKey p) = pretty "DSA" <+> pretty p- pretty (ElGamalPrivateKey p) = pretty "Elgamal" <+> pretty p- pretty (ECDHPrivateKey p) = pretty "ECDH" <+> pretty p- pretty (ECDSAPrivateKey p) = pretty "ECDSA" <+> pretty p- pretty (EdDSAPrivateKey c bs) =- pretty c <+> pretty (bsToHexUpper (BL.fromStrict bs))- pretty (X25519PrivateKey bs) = pretty "X25519" <+> pretty (bsToHexUpper (BL.fromStrict bs))- pretty (X448PrivateKey bs) = pretty "X448" <+> pretty (bsToHexUpper (BL.fromStrict bs))- pretty (UnknownSKey bs) = pretty "<unknown>" <+> pretty (bsToHexUpper bs)+ pretty (RSAPrivateKey p) = pretty "RSA" <+> pretty p+ pretty (DSAPrivateKey p) = pretty "DSA" <+> pretty p+ pretty (ElGamalPrivateKey p) = pretty "Elgamal" <+> pretty p+ pretty (ECDHPrivateKey p) = pretty "ECDH" <+> pretty p+ pretty (ECDSAPrivateKey p) = pretty "ECDSA" <+> pretty p+ pretty (EdDSAPrivateKey c bs) =+ pretty c <+> pretty (bsToHexUpper (BL.fromStrict bs))+ pretty (X25519PrivateKey bs) = pretty "X25519" <+> pretty (bsToHexUpper (BL.fromStrict bs))+ pretty (X448PrivateKey bs) = pretty "X448" <+> pretty (bsToHexUpper (BL.fromStrict bs))+ pretty (MLKEMPrivateKey bs) =+ pretty "ML-KEM-priv" <+> pretty (bsToHexUpper (BL.fromStrict bs))+ pretty (MLDSAPrivateKey bs) =+ pretty "ML-DSA-priv" <+> pretty (bsToHexUpper (BL.fromStrict bs))+ pretty (SLHDSAPrivateKey bs) =+ pretty "SLH-DSA-priv"+ <+> pretty (bsToHexUpper (BL.fromStrict bs))+ pretty (UnknownSKey bs) = pretty "<unknown>" <+> pretty (bsToHexUpper bs) instance A.ToJSON SKey where- toJSON (RSAPrivateKey k) = A.toJSON k- toJSON (DSAPrivateKey k) = A.toJSON k- toJSON (ElGamalPrivateKey k) = A.toJSON k- toJSON (ECDHPrivateKey k) = A.toJSON k- toJSON (ECDSAPrivateKey k) = A.toJSON k- toJSON (EdDSAPrivateKey c bs) = A.toJSON (c, B.unpack bs)- toJSON (X25519PrivateKey bs) = A.toJSON (B.unpack bs)- toJSON (X448PrivateKey bs) = A.toJSON (B.unpack bs)- toJSON (UnknownSKey bs) = A.toJSON (BL.unpack bs)+ toJSON (RSAPrivateKey k) = A.toJSON k+ toJSON (DSAPrivateKey k) = A.toJSON k+ toJSON (ElGamalPrivateKey k) = A.toJSON k+ toJSON (ECDHPrivateKey k) = A.toJSON k+ toJSON (ECDSAPrivateKey k) = A.toJSON k+ toJSON (EdDSAPrivateKey c bs) = A.toJSON (c, B.unpack bs)+ toJSON (X25519PrivateKey bs) = A.toJSON (B.unpack bs)+ toJSON (X448PrivateKey bs) = A.toJSON (B.unpack bs)+ toJSON (MLKEMPrivateKey bs) = A.toJSON (B.unpack bs)+ toJSON (MLDSAPrivateKey bs) = A.toJSON (B.unpack bs)+ toJSON (SLHDSAPrivateKey bs) = A.toJSON (B.unpack bs)+ toJSON (UnknownSKey bs) = A.toJSON (BL.unpack bs) data PKPayload (v :: KeyVersion) where- PKPayloadV3 ::- ThirtyTwoBitTimeStamp- -> V3Expiration- -> PubKeyAlgorithm- -> PKey- -> PKPayload 'DeprecatedV3- PKPayloadV4 ::- ThirtyTwoBitTimeStamp- -> PubKeyAlgorithm- -> PKey- -> PKPayload 'V4- PKPayloadV6 ::- ThirtyTwoBitTimeStamp- -> PubKeyAlgorithm- -> PKey- -> PKPayload 'V6+ PKPayloadV3+ :: ThirtyTwoBitTimeStamp+ -> V3Expiration+ -> PubKeyAlgorithm+ -> PKey+ -> PKPayload 'DeprecatedV3+ PKPayloadV4+ :: ThirtyTwoBitTimeStamp+ -> PubKeyAlgorithm+ -> PKey+ -> PKPayload 'V4+ PKPayloadV6+ :: ThirtyTwoBitTimeStamp+ -> PubKeyAlgorithm+ -> PKey+ -> PKPayload 'V6 deriving instance Eq (PKPayload v) deriving instance Ord (PKPayload v) deriving instance Show (PKPayload v) instance Hashable (PKPayload v) where- hashWithSalt s = hashWithSalt s . pkPayloadFields+ hashWithSalt s = hashWithSalt s . pkPayloadFields instance Pretty (PKPayload v) where- pretty pkp =- let (kv, ts, v3e, pka, p) = pkPayloadFields pkp- in pretty kv <+> pretty ts <+> pretty v3e <+> pretty pka <+> pretty p+ pretty pkp =+ let (kv, ts, v3e, pka, p) = pkPayloadFields pkp+ in pretty kv+ <+> pretty ts+ <+> pretty v3e+ <+> pretty pka+ <+> pretty p instance A.ToJSON (PKPayload v) where- toJSON = A.toJSON . pkPayloadFields+ toJSON = A.toJSON . pkPayloadFields data SomePKPayload where- SomePKPayload :: PKPayload v -> SomePKPayload+ SomePKPayload :: PKPayload v -> SomePKPayload deriving instance Show SomePKPayload deriving instance Typeable SomePKPayload pkPayloadDataType :: DData.DataType pkPayloadDataType =- DData.mkDataType- "Codec.Encryption.OpenPGP.Types.Internal.PKITypes.SomePKPayload"- [pkPayloadConstr]+ DData.mkDataType+ "Codec.Encryption.OpenPGP.Types.Internal.PKITypes.SomePKPayload"+ [pkPayloadConstr] pkPayloadConstr :: DData.Constr pkPayloadConstr = DData.mkConstr pkPayloadDataType "PKPayload" [] DData.Prefix instance Data SomePKPayload where- gfoldl f z (PKPayload kv ts v3e pka p) =- z PKPayload `f` kv `f` ts `f` v3e `f` pka `f` p- gunfold k z c- | c == pkPayloadConstr = k (k (k (k (k (z PKPayload)))))- | otherwise = error "gunfold: invalid constructor for SomePKPayload"- toConstr _ = pkPayloadConstr- dataTypeOf _ = pkPayloadDataType+ gfoldl f z (PKPayload kv ts v3e pka p) =+ z PKPayload `f` kv `f` ts `f` v3e `f` pka `f` p+ gunfold k z c+ | c == pkPayloadConstr = k (k (k (k (k (z PKPayload)))))+ | otherwise =+ error "gunfold: invalid constructor for SomePKPayload"+ toConstr _ = pkPayloadConstr+ dataTypeOf _ = pkPayloadDataType instance Eq SomePKPayload where- a == b = somePKPayloadFields a == somePKPayloadFields b+ a == b = somePKPayloadFields a == somePKPayloadFields b instance Ord SomePKPayload where- compare = comparing somePKPayloadFields+ compare = comparing somePKPayloadFields instance Hashable SomePKPayload where- hashWithSalt s = hashWithSalt s . somePKPayloadFields+ hashWithSalt s = hashWithSalt s . somePKPayloadFields instance Pretty SomePKPayload where- pretty (PKPayload kv ts v3e pka p) =- pretty kv <+> pretty ts <+> pretty v3e <+> pretty pka <+> pretty p+ pretty (PKPayload kv ts v3e pka p) =+ pretty kv+ <+> pretty ts+ <+> pretty v3e+ <+> pretty pka+ <+> pretty p instance A.ToJSON SomePKPayload where- toJSON = A.toJSON . somePKPayloadFields+ toJSON = A.toJSON . somePKPayloadFields -pkPayloadFields ::- PKPayload v- -> (KeyVersion, ThirtyTwoBitTimeStamp, V3Expiration, PubKeyAlgorithm, PKey)+pkPayloadFields+ :: PKPayload v+ -> ( KeyVersion+ , ThirtyTwoBitTimeStamp+ , V3Expiration+ , PubKeyAlgorithm+ , PKey+ ) pkPayloadFields (PKPayloadV3 ts v3e pka p) = (DeprecatedV3, ts, v3e, pka, p) pkPayloadFields (PKPayloadV4 ts pka p) = (V4, ts, 0, pka, p) pkPayloadFields (PKPayloadV6 ts pka p) = (V6, ts, 0, pka, p) -somePKPayloadFields ::- SomePKPayload- -> (KeyVersion, ThirtyTwoBitTimeStamp, V3Expiration, PubKeyAlgorithm, PKey)+somePKPayloadFields+ :: SomePKPayload+ -> ( KeyVersion+ , ThirtyTwoBitTimeStamp+ , V3Expiration+ , PubKeyAlgorithm+ , PKey+ ) somePKPayloadFields (SomePKPayload pkp) = pkPayloadFields pkp -pattern PKPayload ::- KeyVersion- -> ThirtyTwoBitTimeStamp- -> V3Expiration- -> PubKeyAlgorithm- -> PKey- -> SomePKPayload-pattern PKPayload kv ts v3e pka p <- (somePKPayloadFields -> (kv, ts, v3e, pka, p))- where- PKPayload DeprecatedV3 ts v3e pka p = SomePKPayload (PKPayloadV3 ts v3e pka p)- PKPayload V4 ts _ pka p = SomePKPayload (PKPayloadV4 ts pka p)- PKPayload V6 ts _ pka p = SomePKPayload (PKPayloadV6 ts pka p)+pattern PKPayload+ :: KeyVersion+ -> ThirtyTwoBitTimeStamp+ -> V3Expiration+ -> PubKeyAlgorithm+ -> PKey+ -> SomePKPayload+pattern PKPayload kv ts v3e pka p <-+ (somePKPayloadFields -> (kv, ts, v3e, pka, p))+ where+ PKPayload DeprecatedV3 ts v3e pka p = SomePKPayload (PKPayloadV3 ts v3e pka p)+ PKPayload V4 ts _ pka p = SomePKPayload (PKPayloadV4 ts pka p)+ PKPayload V6 ts _ pka p = SomePKPayload (PKPayloadV6 ts pka p) {-# COMPLETE PKPayload #-} @@ -338,67 +301,88 @@ _pubkey (PKPayload _ _ _ _ p) = p data SKAddendum- = SUS16bit SymmetricAlgorithm S2K IV ByteString- | SUSSHA1 SymmetricAlgorithm S2K IV ByteString- | SUSAEAD SymmetricAlgorithm AEADAlgorithm S2K IV ByteString- | SUSym SymmetricAlgorithm IV ByteString- | SUUnencrypted SKey Word16- deriving (Data, Eq, Generic, Show, Typeable)+ = SUS16bit SymmetricAlgorithm S2K IV ByteString+ | SUSSHA1 SymmetricAlgorithm S2K IV ByteString+ | SUSAEAD SymmetricAlgorithm AEADAlgorithm S2K IV ByteString+ | SUSym SymmetricAlgorithm IV ByteString+ | SUUnencrypted SKey Word16+ deriving (Data, Eq, Generic, Show, Typeable) instance Ord SKAddendum where- compare (SUS16bit sa1 s2k1 iv1 bs1) (SUS16bit sa2 s2k2 iv2 bs2) =- compare sa1 sa2 <> compare s2k1 s2k2 <> compare iv1 iv2 <> compare bs1 bs2- compare (SUSSHA1 sa1 s2k1 iv1 bs1) (SUSSHA1 sa2 s2k2 iv2 bs2) =- compare sa1 sa2 <> compare s2k1 s2k2 <> compare iv1 iv2 <> compare bs1 bs2- compare (SUSAEAD sa1 aa1 s2k1 iv1 bs1) (SUSAEAD sa2 aa2 s2k2 iv2 bs2) =- compare sa1 sa2 <> compare aa1 aa2 <> compare s2k1 s2k2 <> compare iv1 iv2 <>- compare bs1 bs2- compare (SUSym sa1 iv1 bs1) (SUSym sa2 iv2 bs2) =- compare sa1 sa2 <> compare iv1 iv2 <> compare bs1 bs2- compare (SUUnencrypted sk1 ck1) (SUUnencrypted sk2 ck2) =- compare sk1 sk2 <> compare ck1 ck2- compare SUS16bit {} SUSSHA1 {} = LT- compare SUS16bit {} SUSAEAD {} = LT- compare SUS16bit {} SUSym {} = LT- compare SUS16bit {} SUUnencrypted {} = LT- compare SUSSHA1 {} SUS16bit {} = GT- compare SUSSHA1 {} SUSAEAD {} = LT- compare SUSSHA1 {} SUSym {} = LT- compare SUSSHA1 {} SUUnencrypted {} = LT- compare SUSAEAD {} SUS16bit {} = GT- compare SUSAEAD {} SUSSHA1 {} = GT- compare SUSAEAD {} SUSym {} = LT- compare SUSAEAD {} SUUnencrypted {} = LT- compare SUSym {} SUS16bit {} = GT- compare SUSym {} SUSSHA1 {} = GT- compare SUSym {} SUSAEAD {} = GT- compare SUSym {} SUUnencrypted {} = LT- compare SUUnencrypted {} _ = GT+ compare (SUS16bit sa1 s2k1 iv1 bs1) (SUS16bit sa2 s2k2 iv2 bs2) =+ compare sa1 sa2+ <> compare s2k1 s2k2+ <> compare iv1 iv2+ <> compare bs1 bs2+ compare (SUSSHA1 sa1 s2k1 iv1 bs1) (SUSSHA1 sa2 s2k2 iv2 bs2) =+ compare sa1 sa2+ <> compare s2k1 s2k2+ <> compare iv1 iv2+ <> compare bs1 bs2+ compare (SUSAEAD sa1 aa1 s2k1 iv1 bs1) (SUSAEAD sa2 aa2 s2k2 iv2 bs2) =+ compare sa1 sa2+ <> compare aa1 aa2+ <> compare s2k1 s2k2+ <> compare iv1 iv2+ <> compare bs1 bs2+ compare (SUSym sa1 iv1 bs1) (SUSym sa2 iv2 bs2) =+ compare sa1 sa2 <> compare iv1 iv2 <> compare bs1 bs2+ compare (SUUnencrypted sk1 ck1) (SUUnencrypted sk2 ck2) =+ compare sk1 sk2 <> compare ck1 ck2+ compare SUS16bit {} SUSSHA1 {} = LT+ compare SUS16bit {} SUSAEAD {} = LT+ compare SUS16bit {} SUSym {} = LT+ compare SUS16bit {} SUUnencrypted {} = LT+ compare SUSSHA1 {} SUS16bit {} = GT+ compare SUSSHA1 {} SUSAEAD {} = LT+ compare SUSSHA1 {} SUSym {} = LT+ compare SUSSHA1 {} SUUnencrypted {} = LT+ compare SUSAEAD {} SUS16bit {} = GT+ compare SUSAEAD {} SUSSHA1 {} = GT+ compare SUSAEAD {} SUSym {} = LT+ compare SUSAEAD {} SUUnencrypted {} = LT+ compare SUSym {} SUS16bit {} = GT+ compare SUSym {} SUSSHA1 {} = GT+ compare SUSym {} SUSAEAD {} = GT+ compare SUSym {} SUUnencrypted {} = LT+ compare SUUnencrypted {} _ = GT instance Hashable SKAddendum instance Pretty SKAddendum where- pretty (SUS16bit sa s2k iv bs) =- pretty "SUS16bit" <+>- pretty sa <+> pretty s2k <+> pretty iv <+> pretty (bsToHexUpper bs)- pretty (SUSSHA1 sa s2k iv bs) =- pretty "SUSSHA1" <+>- pretty sa <+> pretty s2k <+> pretty iv <+> pretty (bsToHexUpper bs)- pretty (SUSAEAD sa aa s2k iv bs) =- pretty "SUSAEAD" <+>- pretty sa <+> pretty aa <+> pretty s2k <+> pretty iv <+>- pretty (bsToHexUpper bs)- pretty (SUSym sa iv bs) =- pretty "SUSym" <+> pretty sa <+> pretty iv <+> pretty (bsToHexUpper bs)- pretty (SUUnencrypted s ck) =- pretty "SUUnencrypted" <+> pretty s <+> pretty ck+ pretty (SUS16bit sa s2k iv bs) =+ pretty "SUS16bit"+ <+> pretty sa+ <+> pretty s2k+ <+> pretty iv+ <+> pretty (bsToHexUpper bs)+ pretty (SUSSHA1 sa s2k iv bs) =+ pretty "SUSSHA1"+ <+> pretty sa+ <+> pretty s2k+ <+> pretty iv+ <+> pretty (bsToHexUpper bs)+ pretty (SUSAEAD sa aa s2k iv bs) =+ pretty "SUSAEAD"+ <+> pretty sa+ <+> pretty aa+ <+> pretty s2k+ <+> pretty iv+ <+> pretty (bsToHexUpper bs)+ pretty (SUSym sa iv bs) =+ pretty "SUSym"+ <+> pretty sa+ <+> pretty iv+ <+> pretty (bsToHexUpper bs)+ pretty (SUUnencrypted s ck) =+ pretty "SUUnencrypted" <+> pretty s <+> pretty ck instance A.ToJSON SKAddendum where- toJSON (SUS16bit sa s2k iv bs) = A.toJSON (sa, s2k, iv, BL.unpack bs)- toJSON (SUSSHA1 sa s2k iv bs) = A.toJSON (sa, s2k, iv, BL.unpack bs)- toJSON (SUSAEAD sa aa s2k iv bs) = A.toJSON (sa, aa, s2k, iv, BL.unpack bs)- toJSON (SUSym sa iv bs) = A.toJSON (sa, iv, BL.unpack bs)- toJSON (SUUnencrypted s ck) = A.toJSON (s, ck)+ toJSON (SUS16bit sa s2k iv bs) = A.toJSON (sa, s2k, iv, BL.unpack bs)+ toJSON (SUSSHA1 sa s2k iv bs) = A.toJSON (sa, s2k, iv, BL.unpack bs)+ toJSON (SUSAEAD sa aa s2k iv bs) = A.toJSON (sa, aa, s2k, iv, BL.unpack bs)+ toJSON (SUSym sa iv bs) = A.toJSON (sa, iv, BL.unpack bs)+ toJSON (SUUnencrypted s ck) = A.toJSON (s, ck) class LegacyKeyVersion (v :: KeyVersion) @@ -407,65 +391,65 @@ instance LegacyKeyVersion 'V4 data SKAddendumV (v :: KeyVersion) where- SKA16bit ::- LegacyKeyVersion v- => SymmetricAlgorithm- -> S2K- -> IV- -> ByteString- -> SKAddendumV v- SKASHA1Legacy ::- LegacyKeyVersion v- => SymmetricAlgorithm- -> S2K- -> IV- -> ByteString- -> SKAddendumV v- SKASHA1V6 ::- SymmetricAlgorithm- -> S2K- -> IV- -> ByteString- -> SKAddendumV 'V6- SKAAEADV6 ::- SymmetricAlgorithm- -> AEADAlgorithm- -> S2K- -> IV- -> ByteString- -> SKAddendumV 'V6- SKAAEADLegacy ::- LegacyKeyVersion v- => SymmetricAlgorithm- -> AEADAlgorithm- -> S2K- -> IV- -> ByteString- -> SKAddendumV v- SKASymLegacy ::- LegacyKeyVersion v- => SymmetricAlgorithm- -> IV- -> ByteString- -> SKAddendumV v- SKASymV6 ::- SymmetricAlgorithm- -> IV- -> ByteString- -> SKAddendumV 'V6- SKAUnencryptedLegacy ::- LegacyKeyVersion v- => SKey- -> Word16- -> SKAddendumV v- SKAUnencryptedV6 ::- SKey- -> SKAddendumV 'V6+ SKA16bit+ :: (LegacyKeyVersion v)+ => SymmetricAlgorithm+ -> S2K+ -> IV+ -> ByteString+ -> SKAddendumV v+ SKASHA1Legacy+ :: (LegacyKeyVersion v)+ => SymmetricAlgorithm+ -> S2K+ -> IV+ -> ByteString+ -> SKAddendumV v+ SKASHA1V6+ :: SymmetricAlgorithm+ -> S2K+ -> IV+ -> ByteString+ -> SKAddendumV 'V6+ SKAAEADV6+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> S2K+ -> IV+ -> ByteString+ -> SKAddendumV 'V6+ SKAAEADLegacy+ :: (LegacyKeyVersion v)+ => SymmetricAlgorithm+ -> AEADAlgorithm+ -> S2K+ -> IV+ -> ByteString+ -> SKAddendumV v+ SKASymLegacy+ :: (LegacyKeyVersion v)+ => SymmetricAlgorithm+ -> IV+ -> ByteString+ -> SKAddendumV v+ SKASymV6+ :: SymmetricAlgorithm+ -> IV+ -> ByteString+ -> SKAddendumV 'V6+ SKAUnencryptedLegacy+ :: (LegacyKeyVersion v)+ => SKey+ -> Word16+ -> SKAddendumV v+ SKAUnencryptedV6+ :: SKey+ -> SKAddendumV 'V6 deriving instance Show (SKAddendumV v) data SomeSKAddendumV where- SomeSKAddendumV :: SKAddendumV v -> SomeSKAddendumV+ SomeSKAddendumV :: SKAddendumV v -> SomeSKAddendumV deriving instance Show SomeSKAddendumV @@ -480,44 +464,69 @@ toSKAddendum (SKAUnencryptedLegacy sk checksum) = SUUnencrypted sk checksum toSKAddendum (SKAUnencryptedV6 sk) = SUUnencrypted sk 0 -fromSKAddendumForKeyVersion ::- KeyVersion- -> SKAddendum- -> Either String SomeSKAddendumV+fromSKAddendumForKeyVersion+ :: KeyVersion+ -> SKAddendum+ -> Either String SomeSKAddendumV fromSKAddendumForKeyVersion DeprecatedV3 (SUS16bit sa s2k iv bs) =- Right (SomeSKAddendumV (SKA16bit sa s2k iv bs :: SKAddendumV 'DeprecatedV3))+ Right+ ( SomeSKAddendumV+ (SKA16bit sa s2k iv bs :: SKAddendumV 'DeprecatedV3)+ ) fromSKAddendumForKeyVersion DeprecatedV3 (SUSSHA1 sa s2k iv bs) =- Right (SomeSKAddendumV (SKASHA1Legacy sa s2k iv bs :: SKAddendumV 'DeprecatedV3))+ Right+ ( SomeSKAddendumV+ (SKASHA1Legacy sa s2k iv bs :: SKAddendumV 'DeprecatedV3)+ ) fromSKAddendumForKeyVersion DeprecatedV3 (SUSym sa iv bs) =- Right (SomeSKAddendumV (SKASymLegacy sa iv bs :: SKAddendumV 'DeprecatedV3))+ Right+ ( SomeSKAddendumV+ (SKASymLegacy sa iv bs :: SKAddendumV 'DeprecatedV3)+ ) fromSKAddendumForKeyVersion DeprecatedV3 (SUUnencrypted sk checksum) =- Right (SomeSKAddendumV (SKAUnencryptedLegacy sk checksum :: SKAddendumV 'DeprecatedV3))+ Right+ ( SomeSKAddendumV+ (SKAUnencryptedLegacy sk checksum :: SKAddendumV 'DeprecatedV3)+ ) fromSKAddendumForKeyVersion DeprecatedV3 (SUSAEAD sa aa s2k iv bs) =- Right (SomeSKAddendumV (SKAAEADLegacy sa aa s2k iv bs :: SKAddendumV 'DeprecatedV3))+ Right+ ( SomeSKAddendumV+ (SKAAEADLegacy sa aa s2k iv bs :: SKAddendumV 'DeprecatedV3)+ ) fromSKAddendumForKeyVersion V4 (SUS16bit sa s2k iv bs) =- Right (SomeSKAddendumV (SKA16bit sa s2k iv bs :: SKAddendumV 'V4))+ Right+ (SomeSKAddendumV (SKA16bit sa s2k iv bs :: SKAddendumV 'V4)) fromSKAddendumForKeyVersion V4 (SUSSHA1 sa s2k iv bs) =- Right (SomeSKAddendumV (SKASHA1Legacy sa s2k iv bs :: SKAddendumV 'V4))+ Right+ (SomeSKAddendumV (SKASHA1Legacy sa s2k iv bs :: SKAddendumV 'V4)) fromSKAddendumForKeyVersion V4 (SUSym sa iv bs) =- Right (SomeSKAddendumV (SKASymLegacy sa iv bs :: SKAddendumV 'V4))+ Right+ (SomeSKAddendumV (SKASymLegacy sa iv bs :: SKAddendumV 'V4)) fromSKAddendumForKeyVersion V4 (SUUnencrypted sk checksum) =- Right (SomeSKAddendumV (SKAUnencryptedLegacy sk checksum :: SKAddendumV 'V4))+ Right+ ( SomeSKAddendumV+ (SKAUnencryptedLegacy sk checksum :: SKAddendumV 'V4)+ ) fromSKAddendumForKeyVersion V4 (SUSAEAD sa aa s2k iv bs) =- Right (SomeSKAddendumV (SKAAEADLegacy sa aa s2k iv bs :: SKAddendumV 'V4))+ Right+ ( SomeSKAddendumV+ (SKAAEADLegacy sa aa s2k iv bs :: SKAddendumV 'V4)+ ) fromSKAddendumForKeyVersion V6 (SUS16bit _ _ _ _) =- Left "v6 secret keys must not use 16-bit checksum protected secret key addendums"+ Left+ "v6 secret keys must not use 16-bit checksum protected secret key addendums" fromSKAddendumForKeyVersion V6 (SUSSHA1 sa s2k iv bs) =- Right (SomeSKAddendumV (SKASHA1V6 sa s2k iv bs))+ Right (SomeSKAddendumV (SKASHA1V6 sa s2k iv bs)) fromSKAddendumForKeyVersion V6 (SUSAEAD sa aa s2k iv bs) =- Right (SomeSKAddendumV (SKAAEADV6 sa aa s2k iv bs))+ Right (SomeSKAddendumV (SKAAEADV6 sa aa s2k iv bs)) fromSKAddendumForKeyVersion V6 (SUSym sa iv bs) =- Right (SomeSKAddendumV (SKASymV6 sa iv bs))+ Right (SomeSKAddendumV (SKASymV6 sa iv bs)) fromSKAddendumForKeyVersion V6 (SUUnencrypted sk _) =- Right (SomeSKAddendumV (SKAUnencryptedV6 sk))+ Right (SomeSKAddendumV (SKAUnencryptedV6 sk)) -fromSKAddendumForPKPayload ::- SomePKPayload- -> SKAddendum- -> Either String SomeSKAddendumV+fromSKAddendumForPKPayload+ :: SomePKPayload+ -> SKAddendum+ -> Either String SomeSKAddendumV fromSKAddendumForPKPayload pkp =- fromSKAddendumForKeyVersion (_keyVersion pkp)+ fromSKAddendumForKeyVersion (_keyVersion pkp)
Codec/Encryption/OpenPGP/Types/Internal/PacketClass.hs view
@@ -2,7 +2,6 @@ -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).- {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-}@@ -16,588 +15,645 @@ module Codec.Encryption.OpenPGP.Types.Internal.PacketClass where -import Codec.Encryption.OpenPGP.Types.Internal.Base-import Codec.Encryption.OpenPGP.Types.Internal.PKITypes-import Codec.Encryption.OpenPGP.Types.Internal.Pkt- import Control.Lens (makeLenses) import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Lazy as BL import Data.Data (Data)+import qualified Data.Kind import Data.List.NonEmpty (NonEmpty) import Data.Text (Text) import Data.Typeable (Typeable) import Data.Word (Word8)-import qualified Data.Kind-import Prettyprinter (Pretty(..))+import Prettyprinter (Pretty (..)) +import Codec.Encryption.OpenPGP.Types.Internal.Base+import Codec.Encryption.OpenPGP.Types.Internal.PKITypes+import Codec.Encryption.OpenPGP.Types.Internal.Pkt+ class Packet a where- data PacketType a :: Data.Kind.Type- packetType :: a -> PacketType a- packetCode :: PacketType a -> Word8- dynamicPacketCode :: a -> Word8- toPkt :: a -> Pkt- fromPktMaybe :: Pkt -> Maybe a- fromPktEither :: Pkt -> Either String a- - fromPktMaybe = either (const Nothing) Just . fromPktEither- dynamicPacketCode = packetCode . packetType+ data PacketType a :: Data.Kind.Type+ packetType :: a -> PacketType a+ packetCode :: PacketType a -> Word8+ dynamicPacketCode :: a -> Word8+ toPkt :: a -> Pkt+ fromPktMaybe :: Pkt -> Maybe a+ fromPktEither :: Pkt -> Either String a + fromPktMaybe = either (const Nothing) Just . fromPktEither+ dynamicPacketCode = packetCode . packetType+ coercionError :: String -> Pkt -> Either String a coercionError expected pkt =- Left- ("Cannot coerce non-" ++- expected ++ " packet (tag " ++ show (pktTag pkt) ++ ")")+ Left+ ( "Cannot coerce non-"+ ++ expected+ ++ " packet (tag "+ ++ show (pktTag pkt)+ ++ ")"+ ) data PKESK (v :: PKESKPayloadVersion) where- PKESK3Packet ::- PacketVersion- -> EightOctetKeyId- -> PubKeyAlgorithm- -> NonEmpty MPI- -> PKESK 'PKESKV3- PKESK6Packet ::- BL.ByteString- -> PubKeyAlgorithm- -> BL.ByteString- -> PKESK 'PKESKV6+ PKESK3Packet+ :: PacketVersion+ -> EightOctetKeyId+ -> PubKeyAlgorithm+ -> NonEmpty MPI+ -> PKESK 'PKESKV3+ PKESK6Packet+ :: BL.ByteString+ -> PubKeyAlgorithm+ -> BL.ByteString+ -> PKESK 'PKESKV6 deriving instance Eq (PKESK v) deriving instance Show (PKESK v) instance Packet (PKESK 'PKESKV3) where- data PacketType (PKESK 'PKESKV3) = PKESKType- deriving (Show, Eq)- packetType _ = PKESKType- packetCode _ = 1- toPkt (PKESK3Packet version keyid pka mpis) =- PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 version keyid pka mpis))- fromPktEither (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ keyid pka mpis))) =- Right (PKESK3Packet 3 keyid pka mpis)- fromPktEither (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 _ _ _))) =- Left "Cannot coerce PKESKv6 packet to PKESKv3"- fromPktEither pkt = coercionError "PKESK" pkt+ data PacketType (PKESK 'PKESKV3) = PKESKType+ deriving (Eq, Show)+ packetType _ = PKESKType+ packetCode _ = 1+ toPkt (PKESK3Packet version keyid pka mpis) =+ PKESKPkt+ (PKESKPayloadV3Packet (PKESKPayloadV3 version keyid pka mpis))+ fromPktEither (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ keyid pka mpis))) =+ Right (PKESK3Packet 3 keyid pka mpis)+ fromPktEither (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 _ _ _))) =+ Left "Cannot coerce PKESKv6 packet to PKESKv3"+ fromPktEither pkt = coercionError "PKESK" pkt instance Pretty (PKESK 'PKESKV3) where- pretty = pretty . toPkt+ pretty = pretty . toPkt instance Packet (PKESK 'PKESKV6) where- data PacketType (PKESK 'PKESKV6) = PKESK6Type- deriving (Show, Eq)- packetType _ = PKESK6Type- packetCode _ = 1- toPkt (PKESK6Packet recipientKeyIdentifier pka esk) =- PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 recipientKeyIdentifier pka esk))- fromPktEither (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 recipientKeyIdentifier pka esk))) =- Right (PKESK6Packet recipientKeyIdentifier pka esk)- fromPktEither (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ _ _ _))) =- Left "Cannot coerce PKESKv3 packet to PKESKv6"- fromPktEither pkt = coercionError "PKESKv6" pkt+ data PacketType (PKESK 'PKESKV6) = PKESK6Type+ deriving (Eq, Show)+ packetType _ = PKESK6Type+ packetCode _ = 1+ toPkt (PKESK6Packet recipientKeyIdentifier pka esk) =+ PKESKPkt+ ( PKESKPayloadV6Packet+ (PKESKPayloadV6 recipientKeyIdentifier pka esk)+ )+ fromPktEither+ ( PKESKPkt+ ( PKESKPayloadV6Packet+ (PKESKPayloadV6 recipientKeyIdentifier pka esk)+ )+ ) =+ Right (PKESK6Packet recipientKeyIdentifier pka esk)+ fromPktEither (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ _ _ _))) =+ Left "Cannot coerce PKESKv3 packet to PKESKv6"+ fromPktEither pkt = coercionError "PKESKv6" pkt instance Pretty (PKESK 'PKESKV6) where- pretty = pretty . toPkt+ pretty = pretty . toPkt -newtype Signature =- Signature+newtype Signature+ = Signature { _signaturePayload :: SignaturePayload }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet Signature where- data PacketType Signature = SignatureType- deriving (Show, Eq)- packetType _ = SignatureType- packetCode _ = 2- toPkt (Signature a) = SignaturePkt a- fromPktMaybe (SignaturePkt a) = Just (Signature a)- fromPktMaybe _ = Nothing- fromPktEither (SignaturePkt a) = Right (Signature a)- fromPktEither pkt = coercionError "Signature" pkt+ data PacketType Signature = SignatureType+ deriving (Eq, Show)+ packetType _ = SignatureType+ packetCode _ = 2+ toPkt (Signature a) = SignaturePkt a+ fromPktMaybe (SignaturePkt a) = Just (Signature a)+ fromPktMaybe _ = Nothing+ fromPktEither (SignaturePkt a) = Right (Signature a)+ fromPktEither pkt = coercionError "Signature" pkt instance Pretty Signature where- pretty = pretty . toPkt+ pretty = pretty . toPkt data SignatureV (v :: SignaturePayloadVersion) where- SignatureV3Packet :: SignaturePayloadV 'SigPayloadV3 -> SignatureV 'SigPayloadV3- SignatureV4Packet :: SignaturePayloadV 'SigPayloadV4 -> SignatureV 'SigPayloadV4- SignatureV6Packet :: SignaturePayloadV 'SigPayloadV6 -> SignatureV 'SigPayloadV6- SignatureVOtherPacket :: SignaturePayloadV 'SigPayloadVOther -> SignatureV 'SigPayloadVOther+ SignatureV3Packet+ :: SignaturePayloadV 'SigPayloadV3 -> SignatureV 'SigPayloadV3+ SignatureV4Packet+ :: SignaturePayloadV 'SigPayloadV4 -> SignatureV 'SigPayloadV4+ SignatureV6Packet+ :: SignaturePayloadV 'SigPayloadV6 -> SignatureV 'SigPayloadV6+ SignatureVOtherPacket+ :: SignaturePayloadV 'SigPayloadVOther+ -> SignatureV 'SigPayloadVOther deriving instance Eq (SignatureV v) deriving instance Show (SignatureV v) data SomeSignatureV where- SomeSignatureV :: SignatureV v -> SomeSignatureV+ SomeSignatureV :: SignatureV v -> SomeSignatureV -signaturePayloadFromSignatureV :: SignatureV v -> SignaturePayload+signaturePayloadFromSignatureV+ :: SignatureV v -> SignaturePayload signaturePayloadFromSignatureV (SignatureV3Packet payload) = toSignaturePayload payload signaturePayloadFromSignatureV (SignatureV4Packet payload) = toSignaturePayload payload signaturePayloadFromSignatureV (SignatureV6Packet payload) = toSignaturePayload payload signaturePayloadFromSignatureV (SignatureVOtherPacket payload) = toSignaturePayload payload -fromPktEitherSomeSignatureV :: Pkt -> Either String SomeSignatureV+fromPktEitherSomeSignatureV+ :: Pkt -> Either String SomeSignatureV fromPktEitherSomeSignatureV (SignaturePkt payload) =- Right (someSignatureVFromPayload payload)+ Right (someSignatureVFromPayload payload) fromPktEitherSomeSignatureV pkt =- coercionError "Signature" pkt+ coercionError "Signature" pkt someSignatureVFromPayload :: SignaturePayload -> SomeSignatureV someSignatureVFromPayload payload =- case toSomeSignaturePayload payload of- SomeSignaturePayload (typedPayload@SigPayloadV3Data {}) ->- SomeSignatureV (SignatureV3Packet typedPayload)- SomeSignaturePayload (typedPayload@SigPayloadV4Data {}) ->- SomeSignatureV (SignatureV4Packet typedPayload)- SomeSignaturePayload (typedPayload@SigPayloadV6Data {}) ->- SomeSignatureV (SignatureV6Packet typedPayload)- SomeSignaturePayload (typedPayload@SigPayloadOtherData {}) ->- SomeSignatureV (SignatureVOtherPacket typedPayload)+ case toSomeSignaturePayload payload of+ SomeSignaturePayload (typedPayload@SigPayloadV3Data {}) ->+ SomeSignatureV (SignatureV3Packet typedPayload)+ SomeSignaturePayload (typedPayload@SigPayloadV4Data {}) ->+ SomeSignatureV (SignatureV4Packet typedPayload)+ SomeSignaturePayload (typedPayload@SigPayloadV6Data {}) ->+ SomeSignatureV (SignatureV6Packet typedPayload)+ SomeSignaturePayload (typedPayload@SigPayloadOtherData {}) ->+ SomeSignatureV (SignatureVOtherPacket typedPayload) instance Packet (SignatureV 'SigPayloadV3) where- data PacketType (SignatureV 'SigPayloadV3) = SignatureV3Type- deriving (Show, Eq)- packetType _ = SignatureV3Type- packetCode _ = 2- toPkt = SignaturePkt . signaturePayloadFromSignatureV- fromPktEither (SignaturePkt payload) =- SignatureV3Packet <$> asSignaturePayloadV3 payload- fromPktEither pkt = coercionError "SignatureV3" pkt+ data PacketType (SignatureV 'SigPayloadV3) = SignatureV3Type+ deriving (Eq, Show)+ packetType _ = SignatureV3Type+ packetCode _ = 2+ toPkt = SignaturePkt . signaturePayloadFromSignatureV+ fromPktEither (SignaturePkt payload) =+ SignatureV3Packet <$> asSignaturePayloadV3 payload+ fromPktEither pkt = coercionError "SignatureV3" pkt instance Pretty (SignatureV 'SigPayloadV3) where- pretty = pretty . toPkt+ pretty = pretty . toPkt instance Packet (SignatureV 'SigPayloadV4) where- data PacketType (SignatureV 'SigPayloadV4) = SignatureV4Type- deriving (Show, Eq)- packetType _ = SignatureV4Type- packetCode _ = 2- toPkt = SignaturePkt . signaturePayloadFromSignatureV- fromPktEither (SignaturePkt payload) =- SignatureV4Packet <$> asSignaturePayloadV4 payload- fromPktEither pkt = coercionError "SignatureV4" pkt+ data PacketType (SignatureV 'SigPayloadV4) = SignatureV4Type+ deriving (Eq, Show)+ packetType _ = SignatureV4Type+ packetCode _ = 2+ toPkt = SignaturePkt . signaturePayloadFromSignatureV+ fromPktEither (SignaturePkt payload) =+ SignatureV4Packet <$> asSignaturePayloadV4 payload+ fromPktEither pkt = coercionError "SignatureV4" pkt instance Pretty (SignatureV 'SigPayloadV4) where- pretty = pretty . toPkt+ pretty = pretty . toPkt instance Packet (SignatureV 'SigPayloadV6) where- data PacketType (SignatureV 'SigPayloadV6) = SignatureV6Type- deriving (Show, Eq)- packetType _ = SignatureV6Type- packetCode _ = 2- toPkt = SignaturePkt . signaturePayloadFromSignatureV- fromPktEither (SignaturePkt payload) =- SignatureV6Packet <$> asSignaturePayloadV6 payload- fromPktEither pkt = coercionError "SignatureV6" pkt+ data PacketType (SignatureV 'SigPayloadV6) = SignatureV6Type+ deriving (Eq, Show)+ packetType _ = SignatureV6Type+ packetCode _ = 2+ toPkt = SignaturePkt . signaturePayloadFromSignatureV+ fromPktEither (SignaturePkt payload) =+ SignatureV6Packet <$> asSignaturePayloadV6 payload+ fromPktEither pkt = coercionError "SignatureV6" pkt instance Pretty (SignatureV 'SigPayloadV6) where- pretty = pretty . toPkt+ pretty = pretty . toPkt instance Packet (SignatureV 'SigPayloadVOther) where- data PacketType (SignatureV 'SigPayloadVOther) = SignatureVOtherType- deriving (Show, Eq)- packetType _ = SignatureVOtherType- packetCode _ = 2- toPkt = SignaturePkt . signaturePayloadFromSignatureV- fromPktEither (SignaturePkt payload) =- SignatureVOtherPacket <$> asSignaturePayloadOther payload- fromPktEither pkt = coercionError "SignatureVOther" pkt+ data PacketType (SignatureV 'SigPayloadVOther) = SignatureVOtherType+ deriving (Eq, Show)+ packetType _ = SignatureVOtherType+ packetCode _ = 2+ toPkt = SignaturePkt . signaturePayloadFromSignatureV+ fromPktEither (SignaturePkt payload) =+ SignatureVOtherPacket <$> asSignaturePayloadOther payload+ fromPktEither pkt = coercionError "SignatureVOther" pkt instance Pretty (SignatureV 'SigPayloadVOther) where- pretty = pretty . toPkt+ pretty = pretty . toPkt data SKESK (v :: SKESKPayloadVersion) where- SKESK4Packet ::- SymmetricAlgorithm- -> S2K- -> Maybe BL.ByteString- -> SKESK 'SKESKV4- SKESK6Packet ::- SymmetricAlgorithm- -> AEADAlgorithm- -> S2K- -> BL.ByteString- -> BL.ByteString- -> BL.ByteString- -> SKESK 'SKESKV6+ SKESK4Packet+ :: SymmetricAlgorithm+ -> S2K+ -> Maybe BL.ByteString+ -> SKESK 'SKESKV4+ SKESK6Packet+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> S2K+ -> BL.ByteString+ -> BL.ByteString+ -> BL.ByteString+ -> SKESK 'SKESKV6 deriving instance Eq (SKESK v) deriving instance Show (SKESK v) instance Packet (SKESK 'SKESKV4) where- data PacketType (SKESK 'SKESKV4) = SKESKType- deriving (Show, Eq)- packetType _ = SKESKType- packetCode _ = 3- toPkt (SKESK4Packet symalgo s2k esk) =- SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 symalgo s2k esk))- fromPktEither (SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 symalgo s2k esk))) =- Right (SKESK4Packet symalgo s2k esk)- fromPktEither (SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 _ _ _ _ _ _))) =- Left "Cannot coerce SKESKv6 packet to SKESKv4"- fromPktEither pkt = coercionError "SKESK" pkt+ data PacketType (SKESK 'SKESKV4) = SKESKType+ deriving (Eq, Show)+ packetType _ = SKESKType+ packetCode _ = 3+ toPkt (SKESK4Packet symalgo s2k esk) =+ SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 symalgo s2k esk))+ fromPktEither (SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 symalgo s2k esk))) =+ Right (SKESK4Packet symalgo s2k esk)+ fromPktEither (SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 _ _ _ _ _ _))) =+ Left "Cannot coerce SKESKv6 packet to SKESKv4"+ fromPktEither pkt = coercionError "SKESK" pkt instance Pretty (SKESK 'SKESKV4) where- pretty = pretty . toPkt+ pretty = pretty . toPkt instance Packet (SKESK 'SKESKV6) where- data PacketType (SKESK 'SKESKV6) = SKESK6Type- deriving (Show, Eq)- packetType _ = SKESK6Type- packetCode _ = 3- toPkt (SKESK6Packet symalgo aead s2k iv esk tag) =- SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 symalgo aead s2k iv esk tag))- fromPktEither (SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 symalgo aead s2k iv esk tag))) =- Right (SKESK6Packet symalgo aead s2k iv esk tag)- fromPktEither (SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 _ _ _))) =- Left "Cannot coerce SKESKv4 packet to SKESKv6"- fromPktEither pkt = coercionError "SKESKv6" pkt+ data PacketType (SKESK 'SKESKV6) = SKESK6Type+ deriving (Eq, Show)+ packetType _ = SKESK6Type+ packetCode _ = 3+ toPkt (SKESK6Packet symalgo aead s2k iv esk tag) =+ SKESKPkt+ (SKESKPayloadV6Packet (SKESKPayloadV6 symalgo aead s2k iv esk tag))+ fromPktEither+ ( SKESKPkt+ (SKESKPayloadV6Packet (SKESKPayloadV6 symalgo aead s2k iv esk tag))+ ) =+ Right (SKESK6Packet symalgo aead s2k iv esk tag)+ fromPktEither (SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 _ _ _))) =+ Left "Cannot coerce SKESKv4 packet to SKESKv6"+ fromPktEither pkt = coercionError "SKESKv6" pkt instance Pretty (SKESK 'SKESKV6) where- pretty = pretty . toPkt+ pretty = pretty . toPkt data OnePassSignature (v :: OnePassSignatureVersion) where- OnePassSignatureV3Packet ::- PacketVersion- -> SigType- -> HashAlgorithm- -> PubKeyAlgorithm- -> EightOctetKeyId- -> NestedFlag- -> OnePassSignature 'OPSV3- OnePassSignatureV6Packet ::- SigType- -> HashAlgorithm- -> PubKeyAlgorithm- -> SignatureSalt- -> ByteString- -> NestedFlag- -> OnePassSignature 'OPSV6+ OnePassSignatureV3Packet+ :: PacketVersion+ -> SigType+ -> HashAlgorithm+ -> PubKeyAlgorithm+ -> EightOctetKeyId+ -> NestedFlag+ -> OnePassSignature 'OPSV3+ OnePassSignatureV6Packet+ :: SigType+ -> HashAlgorithm+ -> PubKeyAlgorithm+ -> SignatureSalt+ -> ByteString+ -> NestedFlag+ -> OnePassSignature 'OPSV6 deriving instance Eq (OnePassSignature v) deriving instance Show (OnePassSignature v) instance Packet (OnePassSignature 'OPSV3) where- data PacketType (OnePassSignature 'OPSV3) = OnePassSignatureType- deriving (Show, Eq)- packetType _ = OnePassSignatureType- packetCode _ = 4- toPkt (OnePassSignatureV3Packet a b c d e f) =- OnePassSignaturePkt (OPSPayloadV3Packet (OPSPayloadV3 a b c d e f))- fromPktEither (OnePassSignaturePkt (OPSPayloadV3Packet (OPSPayloadV3 a b c d e f))) =- Right (OnePassSignatureV3Packet a b c d e f)- fromPktEither pkt = coercionError "OnePassSignature" pkt+ data PacketType (OnePassSignature 'OPSV3) = OnePassSignatureType+ deriving (Eq, Show)+ packetType _ = OnePassSignatureType+ packetCode _ = 4+ toPkt (OnePassSignatureV3Packet a b c d e f) =+ OnePassSignaturePkt+ (OPSPayloadV3Packet (OPSPayloadV3 a b c d e f))+ fromPktEither+ ( OnePassSignaturePkt+ (OPSPayloadV3Packet (OPSPayloadV3 a b c d e f))+ ) =+ Right (OnePassSignatureV3Packet a b c d e f)+ fromPktEither pkt = coercionError "OnePassSignature" pkt instance Pretty (OnePassSignature 'OPSV3) where- pretty = pretty . toPkt+ pretty = pretty . toPkt instance Packet (OnePassSignature 'OPSV6) where- data PacketType (OnePassSignature 'OPSV6) = OnePassSignatureV6Type- deriving (Show, Eq)- packetType _ = OnePassSignatureV6Type- packetCode _ = 4- toPkt (OnePassSignatureV6Packet a b c d e f) =- OnePassSignaturePkt (OPSPayloadV6Packet (OPSPayloadV6 a b c d e f))- fromPktEither (OnePassSignaturePkt (OPSPayloadV6Packet (OPSPayloadV6 a b c d e f))) =- Right (OnePassSignatureV6Packet a b c d e f)- fromPktEither pkt = coercionError "OnePassSignatureV6" pkt+ data PacketType (OnePassSignature 'OPSV6) = OnePassSignatureV6Type+ deriving (Eq, Show)+ packetType _ = OnePassSignatureV6Type+ packetCode _ = 4+ toPkt (OnePassSignatureV6Packet a b c d e f) =+ OnePassSignaturePkt+ (OPSPayloadV6Packet (OPSPayloadV6 a b c d e f))+ fromPktEither+ ( OnePassSignaturePkt+ (OPSPayloadV6Packet (OPSPayloadV6 a b c d e f))+ ) =+ Right (OnePassSignatureV6Packet a b c d e f)+ fromPktEither pkt = coercionError "OnePassSignatureV6" pkt instance Pretty (OnePassSignature 'OPSV6) where- pretty = pretty . toPkt+ pretty = pretty . toPkt -data SecretKey =- SecretKey+data SecretKey+ = SecretKey { _secretKeyPKPayload :: SomePKPayload , _secretKeySKAddendum :: SKAddendum }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet SecretKey where- data PacketType SecretKey = SecretKeyType- deriving (Show, Eq)- packetType _ = SecretKeyType- packetCode _ = 5- toPkt (SecretKey a b) = SecretKeyPkt a b- fromPktEither (SecretKeyPkt a b) = Right (SecretKey a b)- fromPktEither pkt = coercionError "SecretKey" pkt+ data PacketType SecretKey = SecretKeyType+ deriving (Eq, Show)+ packetType _ = SecretKeyType+ packetCode _ = 5+ toPkt (SecretKey a b) = SecretKeyPkt a b+ fromPktEither (SecretKeyPkt a b) = Right (SecretKey a b)+ fromPktEither pkt = coercionError "SecretKey" pkt instance Pretty SecretKey where- pretty = pretty . toPkt+ pretty = pretty . toPkt -newtype PublicKey =- PublicKey+newtype PublicKey+ = PublicKey { _publicKeyPKPayload :: SomePKPayload }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet PublicKey where- data PacketType PublicKey = PublicKeyType- deriving (Show, Eq)- packetType _ = PublicKeyType- packetCode _ = 6- toPkt (PublicKey a) = PublicKeyPkt a- fromPktEither (PublicKeyPkt a) = Right (PublicKey a)- fromPktEither pkt = coercionError "PublicKey" pkt+ data PacketType PublicKey = PublicKeyType+ deriving (Eq, Show)+ packetType _ = PublicKeyType+ packetCode _ = 6+ toPkt (PublicKey a) = PublicKeyPkt a+ fromPktEither (PublicKeyPkt a) = Right (PublicKey a)+ fromPktEither pkt = coercionError "PublicKey" pkt instance Pretty PublicKey where- pretty = pretty . toPkt+ pretty = pretty . toPkt -data SecretSubkey =- SecretSubkey+data SecretSubkey+ = SecretSubkey { _secretSubkeyPKPayload :: SomePKPayload , _secretSubkeySKAddendum :: SKAddendum }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet SecretSubkey where- data PacketType SecretSubkey = SecretSubkeyType- deriving (Show, Eq)- packetType _ = SecretSubkeyType- packetCode _ = 7- toPkt (SecretSubkey a b) = SecretSubkeyPkt a b- fromPktEither (SecretSubkeyPkt a b) = Right (SecretSubkey a b)- fromPktEither pkt = coercionError "SecretSubkey" pkt+ data PacketType SecretSubkey = SecretSubkeyType+ deriving (Eq, Show)+ packetType _ = SecretSubkeyType+ packetCode _ = 7+ toPkt (SecretSubkey a b) = SecretSubkeyPkt a b+ fromPktEither (SecretSubkeyPkt a b) = Right (SecretSubkey a b)+ fromPktEither pkt = coercionError "SecretSubkey" pkt instance Pretty SecretSubkey where- pretty = pretty . toPkt+ pretty = pretty . toPkt -data CompressedData =- CompressedData+data CompressedData+ = CompressedData { _compressedDataCompressionAlgorithm :: CompressionAlgorithm , _compressedDataPayload :: CompressedDataPayload }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet CompressedData where- data PacketType CompressedData = CompressedDataType- deriving (Show, Eq)- packetType _ = CompressedDataType- packetCode _ = 8- toPkt (CompressedData a b) = CompressedDataPkt a b- fromPktEither (CompressedDataPkt a b) = Right (CompressedData a b)- fromPktEither pkt = coercionError "CompressedData" pkt+ data PacketType CompressedData = CompressedDataType+ deriving (Eq, Show)+ packetType _ = CompressedDataType+ packetCode _ = 8+ toPkt (CompressedData a b) = CompressedDataPkt a b+ fromPktEither (CompressedDataPkt a b) = Right (CompressedData a b)+ fromPktEither pkt = coercionError "CompressedData" pkt instance Pretty CompressedData where- pretty = pretty . toPkt+ pretty = pretty . toPkt -newtype SymEncData =- SymEncData+newtype SymEncData+ = SymEncData { _symEncDataPayload :: ByteString }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet SymEncData where- data PacketType SymEncData = SymEncDataType- deriving (Show, Eq)- packetType _ = SymEncDataType- packetCode _ = 9- toPkt (SymEncData a) = SymEncDataPkt a- fromPktEither (SymEncDataPkt a) = Right (SymEncData a)- fromPktEither pkt = coercionError "SymEncData" pkt+ data PacketType SymEncData = SymEncDataType+ deriving (Eq, Show)+ packetType _ = SymEncDataType+ packetCode _ = 9+ toPkt (SymEncData a) = SymEncDataPkt a+ fromPktEither (SymEncDataPkt a) = Right (SymEncData a)+ fromPktEither pkt = coercionError "SymEncData" pkt instance Pretty SymEncData where- pretty = pretty . toPkt+ pretty = pretty . toPkt -newtype Marker =- Marker+newtype Marker+ = Marker { _markerPayload :: ByteString }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet Marker where- data PacketType Marker = MarkerType- deriving (Show, Eq)- packetType _ = MarkerType- packetCode _ = 10- toPkt (Marker a) = MarkerPkt a- fromPktEither (MarkerPkt a) = Right (Marker a)- fromPktEither pkt = coercionError "Marker" pkt+ data PacketType Marker = MarkerType+ deriving (Eq, Show)+ packetType _ = MarkerType+ packetCode _ = 10+ toPkt (Marker a) = MarkerPkt a+ fromPktEither (MarkerPkt a) = Right (Marker a)+ fromPktEither pkt = coercionError "Marker" pkt instance Pretty Marker where- pretty = pretty . toPkt+ pretty = pretty . toPkt -data LiteralData =- LiteralData- { _literalDataDataType :: DataType+data LiteralData+ = LiteralData+ { _literalDataDataType :: LiteralDataType , _literalDataFileName :: FileName , _literalDataTimeStamp :: ThirtyTwoBitTimeStamp , _literalDataPayload :: ByteString }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet LiteralData where- data PacketType LiteralData = LiteralDataType- deriving (Show, Eq)- packetType _ = LiteralDataType- packetCode _ = 11- toPkt (LiteralData a b c d) = LiteralDataPkt a b c d- fromPktEither (LiteralDataPkt a b c d) = Right (LiteralData a b c d)- fromPktEither pkt = coercionError "LiteralData" pkt+ data PacketType LiteralData = LiteralDataType+ deriving (Eq, Show)+ packetType _ = LiteralDataType+ packetCode _ = 11+ toPkt (LiteralData a b c d) = LiteralDataPkt a b c d+ fromPktEither (LiteralDataPkt a b c d) = Right (LiteralData a b c d)+ fromPktEither pkt = coercionError "LiteralData" pkt instance Pretty LiteralData where- pretty = pretty . toPkt+ pretty = pretty . toPkt -newtype Trust =- Trust+newtype Trust+ = Trust { _trustPayload :: ByteString }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet Trust where- data PacketType Trust = TrustType- deriving (Show, Eq)- packetType _ = TrustType- packetCode _ = 12- toPkt (Trust a) = TrustPkt a- fromPktEither (TrustPkt a) = Right (Trust a)- fromPktEither pkt = coercionError "Trust" pkt+ data PacketType Trust = TrustType+ deriving (Eq, Show)+ packetType _ = TrustType+ packetCode _ = 12+ toPkt (Trust a) = TrustPkt a+ fromPktEither (TrustPkt a) = Right (Trust a)+ fromPktEither pkt = coercionError "Trust" pkt instance Pretty Trust where- pretty = pretty . toPkt+ pretty = pretty . toPkt -newtype UserId =- UserId+newtype UserId+ = UserId { _userIdPayload :: Text }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet UserId where- data PacketType UserId = UserIdType- deriving (Show, Eq)- packetType _ = UserIdType- packetCode _ = 13- toPkt (UserId a) = UserIdPkt a- fromPktEither (UserIdPkt a) = Right (UserId a)- fromPktEither pkt = coercionError "UserId" pkt+ data PacketType UserId = UserIdType+ deriving (Eq, Show)+ packetType _ = UserIdType+ packetCode _ = 13+ toPkt (UserId a) = UserIdPkt a+ fromPktEither (UserIdPkt a) = Right (UserId a)+ fromPktEither pkt = coercionError "UserId" pkt instance Pretty UserId where- pretty = pretty . toPkt+ pretty = pretty . toPkt -newtype PublicSubkey =- PublicSubkey+newtype PublicSubkey+ = PublicSubkey { _publicSubkeyPKPayload :: SomePKPayload }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet PublicSubkey where- data PacketType PublicSubkey = PublicSubkeyType- deriving (Show, Eq)- packetType _ = PublicSubkeyType- packetCode _ = 14- toPkt (PublicSubkey a) = PublicSubkeyPkt a- fromPktEither (PublicSubkeyPkt a) = Right (PublicSubkey a)- fromPktEither pkt = coercionError "PublicSubkey" pkt+ data PacketType PublicSubkey = PublicSubkeyType+ deriving (Eq, Show)+ packetType _ = PublicSubkeyType+ packetCode _ = 14+ toPkt (PublicSubkey a) = PublicSubkeyPkt a+ fromPktEither (PublicSubkeyPkt a) = Right (PublicSubkey a)+ fromPktEither pkt = coercionError "PublicSubkey" pkt instance Pretty PublicSubkey where- pretty = pretty . toPkt+ pretty = pretty . toPkt -newtype UserAttribute =- UserAttribute+newtype UserAttribute+ = UserAttribute { _userAttributeSubPackets :: [UserAttrSubPacket] }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet UserAttribute where- data PacketType UserAttribute = UserAttributeType- deriving (Show, Eq)- packetType _ = UserAttributeType- packetCode _ = 17- toPkt (UserAttribute a) = UserAttributePkt a- fromPktEither (UserAttributePkt a) = Right (UserAttribute a)- fromPktEither pkt = coercionError "UserAttribute" pkt+ data PacketType UserAttribute = UserAttributeType+ deriving (Eq, Show)+ packetType _ = UserAttributeType+ packetCode _ = 17+ toPkt (UserAttribute a) = UserAttributePkt a+ fromPktEither (UserAttributePkt a) = Right (UserAttribute a)+ fromPktEither pkt = coercionError "UserAttribute" pkt instance Pretty UserAttribute where- pretty = pretty . toPkt+ pretty = pretty . toPkt -data SymEncIntegrityProtectedData =- SymEncIntegrityProtectedData- { _symEncIntegrityProtectedDataPacketVersion :: PacketVersion- , _symEncIntegrityProtectedDataPayload :: ByteString- }- | SymEncIntegrityProtectedDataV2- SymmetricAlgorithm- AEADAlgorithm- Word8- Salt- ByteString- deriving (Data, Eq, Show, Typeable)+data SymEncIntegrityProtectedData+ = SymEncIntegrityProtectedData+ { _symEncIntegrityProtectedDataPacketVersion :: PacketVersion+ , _symEncIntegrityProtectedDataPayload :: ByteString+ }+ | SymEncIntegrityProtectedDataV2+ SymmetricAlgorithm+ AEADAlgorithm+ Word8+ Salt+ ByteString+ deriving (Data, Eq, Show, Typeable) instance Packet SymEncIntegrityProtectedData where- data PacketType- SymEncIntegrityProtectedData = SymEncIntegrityProtectedDataType- deriving (Show, Eq)- packetType _ = SymEncIntegrityProtectedDataType- packetCode _ = 18- toPkt (SymEncIntegrityProtectedData a b) = SymEncIntegrityProtectedDataPkt (SEIPD1 a b)- toPkt (SymEncIntegrityProtectedDataV2 a b c d e) =- SymEncIntegrityProtectedDataPkt (SEIPD2 a b c d e)- fromPktEither (SymEncIntegrityProtectedDataPkt (SEIPD1 a b)) =- Right (SymEncIntegrityProtectedData a b)- fromPktEither (SymEncIntegrityProtectedDataPkt (SEIPD2 a b c d e)) =- Right (SymEncIntegrityProtectedDataV2 a b c d e)- fromPktEither pkt = coercionError "SymEncIntegrityProtectedData" pkt+ data+ PacketType+ SymEncIntegrityProtectedData+ = SymEncIntegrityProtectedDataType+ deriving (Eq, Show)+ packetType _ = SymEncIntegrityProtectedDataType+ packetCode _ = 18+ toPkt (SymEncIntegrityProtectedData a b) = SymEncIntegrityProtectedDataPkt (SEIPD1 a b)+ toPkt (SymEncIntegrityProtectedDataV2 a b c d e) =+ SymEncIntegrityProtectedDataPkt (SEIPD2 a b c d e)+ fromPktEither (SymEncIntegrityProtectedDataPkt (SEIPD1 a b)) =+ Right (SymEncIntegrityProtectedData a b)+ fromPktEither (SymEncIntegrityProtectedDataPkt (SEIPD2 a b c d e)) =+ Right (SymEncIntegrityProtectedDataV2 a b c d e)+ fromPktEither pkt = coercionError "SymEncIntegrityProtectedData" pkt instance Pretty SymEncIntegrityProtectedData where- pretty = pretty . toPkt+ pretty = pretty . toPkt -newtype ModificationDetectionCode =- ModificationDetectionCode+newtype ModificationDetectionCode+ = ModificationDetectionCode { _modificationDetectionCodePayload :: ByteString }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet ModificationDetectionCode where- data PacketType- ModificationDetectionCode = ModificationDetectionCodeType- deriving (Show, Eq)- packetType _ = ModificationDetectionCodeType- packetCode _ = 19- toPkt (ModificationDetectionCode a) = ModificationDetectionCodePkt a- fromPktEither (ModificationDetectionCodePkt a) = Right (ModificationDetectionCode a)- fromPktEither pkt = coercionError "ModificationDetectionCode" pkt+ data+ PacketType+ ModificationDetectionCode+ = ModificationDetectionCodeType+ deriving (Eq, Show)+ packetType _ = ModificationDetectionCodeType+ packetCode _ = 19+ toPkt (ModificationDetectionCode a) = ModificationDetectionCodePkt a+ fromPktEither (ModificationDetectionCodePkt a) = Right (ModificationDetectionCode a)+ fromPktEither pkt = coercionError "ModificationDetectionCode" pkt instance Pretty ModificationDetectionCode where- pretty = pretty . toPkt+ pretty = pretty . toPkt -data OtherPacket =- OtherPacket+newtype Padding+ = Padding+ { _padding :: ByteString+ }+ deriving (Data, Eq, Show, Typeable)++instance Packet Padding where+ data+ PacketType+ Padding+ = PaddingType+ deriving (Eq, Show)+ packetType _ = PaddingType+ packetCode _ = 21+ toPkt (Padding a) = PaddingPkt a+ fromPktEither (PaddingPkt a) = Right (Padding a)+ fromPktEither pkt = coercionError "Padding" pkt++instance Pretty Padding where+ pretty = pretty . toPkt++data OtherPacket+ = OtherPacket { _otherPacketType :: Word8 , _otherPacketPayload :: ByteString }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet OtherPacket where- data PacketType OtherPacket = OtherPacketType- deriving (Show, Eq)- packetType _ = OtherPacketType- packetCode _ = error "OtherPacket has no static packet type code"- dynamicPacketCode = _otherPacketType- toPkt (OtherPacket a b) = OtherPacketPkt a b- fromPktEither (OtherPacketPkt a b) = Right (OtherPacket a b)- fromPktEither pkt = coercionError "OtherPacket" pkt+ data PacketType OtherPacket = OtherPacketType+ deriving (Eq, Show)+ packetType _ = OtherPacketType+ packetCode _ = error "OtherPacket has no static packet type code"+ dynamicPacketCode = _otherPacketType+ toPkt (OtherPacket a b) = OtherPacketPkt a b+ fromPktEither (OtherPacketPkt a b) = Right (OtherPacket a b)+ fromPktEither pkt = coercionError "OtherPacket" pkt instance Pretty OtherPacket where- pretty = pretty . toPkt+ pretty = pretty . toPkt -data BrokenPacket =- BrokenPacket+data BrokenPacket+ = BrokenPacket { _brokenPacketParseError :: String , _brokenPacketType :: Word8 , _brokenPacketPayload :: ByteString }- deriving (Data, Eq, Show, Typeable)+ deriving (Data, Eq, Show, Typeable) instance Packet BrokenPacket where- data PacketType BrokenPacket = BrokenPacketType- deriving (Show, Eq)- packetType _ = BrokenPacketType- packetCode _ = error "BrokenPacket has no static packet type code"- dynamicPacketCode = _brokenPacketType- toPkt (BrokenPacket a b c) = BrokenPacketPkt a b c- fromPktEither (BrokenPacketPkt a b c) = Right (BrokenPacket a b c)- fromPktEither pkt = coercionError "BrokenPacket" pkt+ data PacketType BrokenPacket = BrokenPacketType+ deriving (Eq, Show)+ packetType _ = BrokenPacketType+ packetCode _ = error "BrokenPacket has no static packet type code"+ dynamicPacketCode = _brokenPacketType+ toPkt (BrokenPacket a b c) = BrokenPacketPkt a b c+ fromPktEither (BrokenPacketPkt a b c) = Right (BrokenPacket a b c)+ fromPktEither pkt = coercionError "BrokenPacket" pkt instance Pretty BrokenPacket where- pretty = pretty . toPkt+ pretty = pretty . toPkt $(makeLenses ''Signature)
Codec/Encryption/OpenPGP/Types/Internal/Pkt.hs view
@@ -2,470 +2,571 @@ -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).- {-# LANGUAGE DataKinds #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE KindSignatures #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE KindSignatures #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} module Codec.Encryption.OpenPGP.Types.Internal.Pkt where -import GHC.Generics (Generic)--import Codec.Encryption.OpenPGP.Types.Internal.Base-import Codec.Encryption.OpenPGP.Types.Internal.PKITypes--import Codec.Encryption.OpenPGP.Types.Internal.PrettyUtils (prettyLBS) import Control.Lens (makeLenses)-import Data.Aeson ((.=), object)+import Data.Aeson (object, (.=)) import qualified Data.Aeson as A+import qualified Data.Aeson.Key as AK import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Lazy as BL-import Data.Data (Data(..), Constr, mkDataType, mkConstr, Fixity(Prefix))+import Data.Data+ ( Constr+ , Data (..)+ , Fixity (Prefix)+ , mkConstr+ , mkDataType+ ) import qualified Data.Data as DD-import Data.Hashable (Hashable(..))+import Data.Hashable (Hashable (..)) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as NE import Data.Ord (comparing) import Data.Text (Text)-import qualified Data.Text as T import Data.Time.Clock (UTCTime) import Data.Typeable (Typeable) import Data.Word (Word8)-import Prettyprinter (Pretty(..), (<+>))+import GHC.Generics (Generic)+import Prettyprinter (Pretty (..), (<+>)) +import Codec.Encryption.OpenPGP.Types.Internal.Base+import Codec.Encryption.OpenPGP.Types.Internal.PKITypes+import Codec.Encryption.OpenPGP.Types.Internal.PrettyUtils+ ( prettyLBS+ )+ data PKESKPayloadVersion = PKESKV3 | PKESKV6- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) -data PKESKPayloadV3 =- PKESKPayloadV3- PacketVersion- EightOctetKeyId- PubKeyAlgorithm- (NonEmpty MPI)- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+data PKESKPayloadV3+ = PKESKPayloadV3+ PacketVersion+ EightOctetKeyId+ PubKeyAlgorithm+ (NonEmpty MPI)+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) -data PKESKPayloadV6 =- PKESKPayloadV6- BL.ByteString- PubKeyAlgorithm- BL.ByteString- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+data PKESKPayloadV6+ = PKESKPayloadV6+ BL.ByteString+ PubKeyAlgorithm+ BL.ByteString+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) data PKESKPayload- = PKESKPayloadV3Packet PKESKPayloadV3- | PKESKPayloadV6Packet PKESKPayloadV6- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+ = PKESKPayloadV3Packet PKESKPayloadV3+ | PKESKPayloadV6Packet PKESKPayloadV6+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) data SKESKPayloadVersion = SKESKV4 | SKESKV6- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) -data SKESKPayloadV4 =- SKESKPayloadV4- SymmetricAlgorithm- S2K- (Maybe BL.ByteString)- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+data SKESKPayloadV4+ = SKESKPayloadV4+ SymmetricAlgorithm+ S2K+ (Maybe BL.ByteString)+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) -data SKESKPayloadV6 =- SKESKPayloadV6- SymmetricAlgorithm- AEADAlgorithm- S2K- BL.ByteString- BL.ByteString- BL.ByteString- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+data SKESKPayloadV6+ = SKESKPayloadV6+ SymmetricAlgorithm+ AEADAlgorithm+ S2K+ BL.ByteString+ BL.ByteString+ BL.ByteString+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) data SKESKPayload- = SKESKPayloadV4Packet SKESKPayloadV4- | SKESKPayloadV6Packet SKESKPayloadV6- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+ = SKESKPayloadV4Packet SKESKPayloadV4+ | SKESKPayloadV6Packet SKESKPayloadV6+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) data OnePassSignatureVersion = OPSV3 | OPSV6- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) -data OPSPayloadV3 =- OPSPayloadV3- PacketVersion- SigType- HashAlgorithm- PubKeyAlgorithm- EightOctetKeyId- NestedFlag- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+data OPSPayloadV3+ = OPSPayloadV3+ PacketVersion+ SigType+ HashAlgorithm+ PubKeyAlgorithm+ EightOctetKeyId+ NestedFlag+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) -data OPSPayloadV6 =- OPSPayloadV6- SigType- HashAlgorithm- PubKeyAlgorithm- SignatureSalt- BL.ByteString- NestedFlag- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+data OPSPayloadV6+ = OPSPayloadV6+ SigType+ HashAlgorithm+ PubKeyAlgorithm+ SignatureSalt+ BL.ByteString+ NestedFlag+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) data OnePassSignaturePayload- = OPSPayloadV3Packet OPSPayloadV3- | OPSPayloadV6Packet OPSPayloadV6- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+ = OPSPayloadV3Packet OPSPayloadV3+ | OPSPayloadV6Packet OPSPayloadV6+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) data SEIPDPayload- = SEIPD1 PacketVersion BL.ByteString- | SEIPD2 SymmetricAlgorithm AEADAlgorithm Word8 Salt BL.ByteString- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+ = SEIPD1 PacketVersion BL.ByteString+ | SEIPD2 SymmetricAlgorithm AEADAlgorithm Word8 Salt BL.ByteString+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) data KeyPktKind- = PublicPkt- | SecretPkt- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+ = PublicPkt+ | SecretPkt+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) data KeyPktRole- = KeyPktPrimary- | KeyPktSubkey- deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable)+ = KeyPktPrimary+ | KeyPktSubkey+ deriving (Data, Eq, Generic, Hashable, Ord, Show, Typeable) data KeyPkt (k :: KeyPktKind) where- KeyPktPublicPrimary :: SomePKPayload -> KeyPkt 'PublicPkt- KeyPktPublicSubkey :: SomePKPayload -> KeyPkt 'PublicPkt- KeyPktSecretPrimary :: SomePKPayload -> SKAddendum -> KeyPkt 'SecretPkt- KeyPktSecretSubkey :: SomePKPayload -> SKAddendum -> KeyPkt 'SecretPkt+ KeyPktPublicPrimary :: SomePKPayload -> KeyPkt 'PublicPkt+ KeyPktPublicSubkey :: SomePKPayload -> KeyPkt 'PublicPkt+ KeyPktSecretPrimary+ :: SomePKPayload -> SKAddendum -> KeyPkt 'SecretPkt+ KeyPktSecretSubkey+ :: SomePKPayload -> SKAddendum -> KeyPkt 'SecretPkt deriving instance Eq (KeyPkt k) deriving instance Show (KeyPkt k) instance Ord (KeyPkt k) where- compare = comparing keyPktToPkt+ compare = comparing keyPktToPkt instance Hashable (KeyPkt k) where- hashWithSalt s kp = hashWithSalt s (keyPktToPkt kp)+ hashWithSalt s kp = hashWithSalt s (keyPktToPkt kp) instance Typeable k => Data (KeyPkt k) where- gfoldl f z (KeyPktPublicPrimary pkp) = z KeyPktPublicPrimary `f` pkp- gfoldl f z (KeyPktPublicSubkey pkp) = z KeyPktPublicSubkey `f` pkp- gfoldl f z (KeyPktSecretPrimary pkp ska) = z KeyPktSecretPrimary `f` pkp `f` ska- gfoldl f z (KeyPktSecretSubkey pkp ska) = z KeyPktSecretSubkey `f` pkp `f` ska+ gfoldl f z (KeyPktPublicPrimary pkp) = z KeyPktPublicPrimary `f` pkp+ gfoldl f z (KeyPktPublicSubkey pkp) = z KeyPktPublicSubkey `f` pkp+ gfoldl f z (KeyPktSecretPrimary pkp ska) = z KeyPktSecretPrimary `f` pkp `f` ska+ gfoldl f z (KeyPktSecretSubkey pkp ska) = z KeyPktSecretSubkey `f` pkp `f` ska - toConstr (KeyPktPublicPrimary _) = conKeyPktPublicPrimary- toConstr (KeyPktPublicSubkey _) = conKeyPktPublicSubkey- toConstr (KeyPktSecretPrimary _ _) = conKeyPktSecretPrimary- toConstr (KeyPktSecretSubkey _ _) = conKeyPktSecretSubkey+ toConstr (KeyPktPublicPrimary _) = conKeyPktPublicPrimary+ toConstr (KeyPktPublicSubkey _) = conKeyPktPublicSubkey+ toConstr (KeyPktSecretPrimary _ _) = conKeyPktSecretPrimary+ toConstr (KeyPktSecretSubkey _ _) = conKeyPktSecretSubkey - dataTypeOf _ = tyKeyPkt+ dataTypeOf _ = tyKeyPkt - gunfold _ _ _ = error "KeyPkt: gunfold not supported for GADT"+ gunfold _ _ _ = error "KeyPkt: gunfold not supported for GADT" tyKeyPkt :: DD.DataType-tyKeyPkt = mkDataType "Codec.Encryption.OpenPGP.Types.Internal.Pkt.KeyPkt"- [ conKeyPktPublicPrimary- , conKeyPktPublicSubkey- , conKeyPktSecretPrimary- , conKeyPktSecretSubkey- ]+tyKeyPkt =+ mkDataType+ "Codec.Encryption.OpenPGP.Types.Internal.Pkt.KeyPkt"+ [ conKeyPktPublicPrimary+ , conKeyPktPublicSubkey+ , conKeyPktSecretPrimary+ , conKeyPktSecretSubkey+ ] -conKeyPktPublicPrimary, conKeyPktPublicSubkey,- conKeyPktSecretPrimary, conKeyPktSecretSubkey :: Constr+conKeyPktPublicPrimary+ , conKeyPktPublicSubkey+ , conKeyPktSecretPrimary+ , conKeyPktSecretSubkey+ :: Constr conKeyPktPublicPrimary = mkConstr tyKeyPkt "KeyPktPublicPrimary" [] Prefix-conKeyPktPublicSubkey = mkConstr tyKeyPkt "KeyPktPublicSubkey" [] Prefix+conKeyPktPublicSubkey = mkConstr tyKeyPkt "KeyPktPublicSubkey" [] Prefix conKeyPktSecretPrimary = mkConstr tyKeyPkt "KeyPktSecretPrimary" [] Prefix-conKeyPktSecretSubkey = mkConstr tyKeyPkt "KeyPktSecretSubkey" [] Prefix+conKeyPktSecretSubkey = mkConstr tyKeyPkt "KeyPktSecretSubkey" [] Prefix data SomeKeyPkt where- SomeKeyPkt :: KeyPkt k -> SomeKeyPkt+ SomeKeyPkt :: KeyPkt k -> SomeKeyPkt deriving instance Show SomeKeyPkt instance Eq SomeKeyPkt where- SomeKeyPkt left == SomeKeyPkt right = someKeyPktToPkt (SomeKeyPkt left) == someKeyPktToPkt (SomeKeyPkt right)+ SomeKeyPkt left == SomeKeyPkt right =+ someKeyPktToPkt (SomeKeyPkt left)+ == someKeyPktToPkt (SomeKeyPkt right) data KeyPktCoercionError- = NotAKeyPacket Pkt- | ExpectedPublicKeyPacket Pkt- | ExpectedSecretKeyPacket Pkt- deriving (Eq, Show)+ = NotAKeyPacket Pkt+ | ExpectedPublicKeyPacket Pkt+ | ExpectedSecretKeyPacket Pkt+ deriving (Eq, Show) -- data Pkt = forall a. (Packet a, Show a, Eq a) => Pkt a data Pkt- = PKESKPkt PKESKPayload- | SignaturePkt SignaturePayload- | SKESKPkt SKESKPayload- | OnePassSignaturePkt OnePassSignaturePayload- | SecretKeyPkt SomePKPayload SKAddendum- | PublicKeyPkt SomePKPayload- | SecretSubkeyPkt SomePKPayload SKAddendum- | CompressedDataPkt CompressionAlgorithm CompressedDataPayload- | SymEncDataPkt ByteString- | MarkerPkt ByteString- | LiteralDataPkt DataType FileName ThirtyTwoBitTimeStamp ByteString- | TrustPkt ByteString- | UserIdPkt Text- | PublicSubkeyPkt SomePKPayload- | UserAttributePkt [UserAttrSubPacket]- | SymEncIntegrityProtectedDataPkt SEIPDPayload- | ModificationDetectionCodePkt ByteString- | OtherPacketPkt Word8 ByteString- | BrokenPacketPkt String Word8 ByteString- deriving (Data, Eq, Generic, Show, Typeable)+ = PKESKPkt PKESKPayload+ | SignaturePkt SignaturePayload+ | SKESKPkt SKESKPayload+ | OnePassSignaturePkt OnePassSignaturePayload+ | SecretKeyPkt SomePKPayload SKAddendum+ | PublicKeyPkt SomePKPayload+ | SecretSubkeyPkt SomePKPayload SKAddendum+ | CompressedDataPkt CompressionAlgorithm CompressedDataPayload+ | SymEncDataPkt ByteString+ | MarkerPkt ByteString+ | LiteralDataPkt+ LiteralDataType+ FileName+ ThirtyTwoBitTimeStamp+ ByteString+ | TrustPkt ByteString+ | UserIdPkt Text+ | PublicSubkeyPkt SomePKPayload+ | UserAttributePkt [UserAttrSubPacket]+ | SymEncIntegrityProtectedDataPkt SEIPDPayload+ | ModificationDetectionCodePkt ByteString+ | PaddingPkt ByteString+ | OtherPacketPkt Word8 ByteString+ | BrokenPacketPkt String Word8 ByteString+ deriving (Data, Eq, Generic, Show, Typeable) -data PktWithWireRep =- PktWithWireRep+data PktWithWireRep+ = PktWithWireRep { _pktWireRepRef :: WireRepRef , _pktRange :: ByteRange , _pktRaw :: ByteString , _pktIndex :: Int , _pktValue :: Pkt }- deriving (Data, Eq, Generic, Show, Typeable)+ deriving (Data, Eq, Generic, Show, Typeable) instance Hashable Pkt instance Ord Pkt where- compare p1 p2 = comparing pktTag p1 p2 <> compareFields p1 p2- where- compareFields (PKESKPkt pkesk1) (PKESKPkt pkesk2) = compare pkesk1 pkesk2- compareFields (SignaturePkt sp1) (SignaturePkt sp2) = compare sp1 sp2- compareFields (SKESKPkt skesk1) (SKESKPkt skesk2) = compare skesk1 skesk2- compareFields (OnePassSignaturePkt ops1) (OnePassSignaturePkt ops2) = compare ops1 ops2- compareFields (SecretKeyPkt pkp1 ska1) (SecretKeyPkt pkp2 ska2) =- compare pkp1 pkp2 <> compare ska1 ska2- compareFields (PublicKeyPkt pkp1) (PublicKeyPkt pkp2) = compare pkp1 pkp2- compareFields (SecretSubkeyPkt pkp1 ska1) (SecretSubkeyPkt pkp2 ska2) =- compare pkp1 pkp2 <> compare ska1 ska2- compareFields (CompressedDataPkt ca1 cdp1) (CompressedDataPkt ca2 cdp2) =- compare ca1 ca2 <> compare cdp1 cdp2- compareFields (SymEncDataPkt bs1) (SymEncDataPkt bs2) = compare bs1 bs2- compareFields (MarkerPkt bs1) (MarkerPkt bs2) = compare bs1 bs2- compareFields (LiteralDataPkt dt1 fn1 ts1 bs1) (LiteralDataPkt dt2 fn2 ts2 bs2) =- compare dt1 dt2 <> compare fn1 fn2 <> compare ts1 ts2 <> compare bs1 bs2- compareFields (TrustPkt bs1) (TrustPkt bs2) = compare bs1 bs2- compareFields (UserIdPkt u1) (UserIdPkt u2) = compare u1 u2- compareFields (PublicSubkeyPkt pkp1) (PublicSubkeyPkt pkp2) = compare pkp1 pkp2- compareFields (UserAttributePkt us1) (UserAttributePkt us2) = compare us1 us2- compareFields (SymEncIntegrityProtectedDataPkt seipd1) (SymEncIntegrityProtectedDataPkt seipd2) = compare seipd1 seipd2- compareFields (ModificationDetectionCodePkt bs1) (ModificationDetectionCodePkt bs2) = compare bs1 bs2- compareFields (OtherPacketPkt t1 bs1) (OtherPacketPkt t2 bs2) = compare t1 t2 <> compare bs1 bs2- compareFields (BrokenPacketPkt s1 t1 bs1) (BrokenPacketPkt s2 t2 bs2) =- compare s1 s2 <> compare t1 t2 <> compare bs1 bs2- compareFields _ _ = EQ+ compare p1 p2 = comparing pktTag p1 p2 <> compareFields p1 p2+ where+ compareFields (PKESKPkt pkesk1) (PKESKPkt pkesk2) = compare pkesk1 pkesk2+ compareFields (SignaturePkt sp1) (SignaturePkt sp2) = compare sp1 sp2+ compareFields (SKESKPkt skesk1) (SKESKPkt skesk2) = compare skesk1 skesk2+ compareFields (OnePassSignaturePkt ops1) (OnePassSignaturePkt ops2) = compare ops1 ops2+ compareFields (SecretKeyPkt pkp1 ska1) (SecretKeyPkt pkp2 ska2) =+ compare pkp1 pkp2 <> compare ska1 ska2+ compareFields (PublicKeyPkt pkp1) (PublicKeyPkt pkp2) = compare pkp1 pkp2+ compareFields (SecretSubkeyPkt pkp1 ska1) (SecretSubkeyPkt pkp2 ska2) =+ compare pkp1 pkp2 <> compare ska1 ska2+ compareFields (CompressedDataPkt ca1 cdp1) (CompressedDataPkt ca2 cdp2) =+ compare ca1 ca2 <> compare cdp1 cdp2+ compareFields (SymEncDataPkt bs1) (SymEncDataPkt bs2) = compare bs1 bs2+ compareFields (MarkerPkt bs1) (MarkerPkt bs2) = compare bs1 bs2+ compareFields (LiteralDataPkt dt1 fn1 ts1 bs1) (LiteralDataPkt dt2 fn2 ts2 bs2) =+ compare dt1 dt2+ <> compare fn1 fn2+ <> compare ts1 ts2+ <> compare bs1 bs2+ compareFields (TrustPkt bs1) (TrustPkt bs2) = compare bs1 bs2+ compareFields (UserIdPkt u1) (UserIdPkt u2) = compare u1 u2+ compareFields (PublicSubkeyPkt pkp1) (PublicSubkeyPkt pkp2) = compare pkp1 pkp2+ compareFields (UserAttributePkt us1) (UserAttributePkt us2) = compare us1 us2+ compareFields (SymEncIntegrityProtectedDataPkt seipd1) (SymEncIntegrityProtectedDataPkt seipd2) = compare seipd1 seipd2+ compareFields (ModificationDetectionCodePkt bs1) (ModificationDetectionCodePkt bs2) = compare bs1 bs2+ compareFields (OtherPacketPkt t1 bs1) (OtherPacketPkt t2 bs2) = compare t1 t2 <> compare bs1 bs2+ compareFields (BrokenPacketPkt s1 t1 bs1) (BrokenPacketPkt s2 t2 bs2) =+ compare s1 s2 <> compare t1 t2 <> compare bs1 bs2+ compareFields _ _ = EQ instance Ord PktWithWireRep where- compare p1 p2 =- comparing _pktValue p1 p2 <>- comparing _pktRaw p1 p2 <>- comparing _pktWireRepRef p1 p2 <>- comparing _pktRange p1 p2 <>- comparing _pktIndex p1 p2+ compare p1 p2 =+ comparing _pktValue p1 p2+ <> comparing _pktRaw p1 p2+ <> comparing _pktWireRepRef p1 p2+ <> comparing _pktRange p1 p2+ <> comparing _pktIndex p1 p2 wireRepOfPkt :: PktWithWireRep -> WireRepRef wireRepOfPkt = _pktWireRepRef -packetsFromWireRep :: WireRepRef -> [PktWithWireRep] -> [PktWithWireRep]+packetsFromWireRep+ :: WireRepRef -> [PktWithWireRep] -> [PktWithWireRep] packetsFromWireRep src = filter ((== src) . wireRepOfPkt) instance Pretty Pkt where- pretty (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 pv eoki pka mpis))) =- pretty "PKESK v" <> pretty pv <> pretty ':' <+>- pretty eoki <+> pretty pka <+> (pretty . NE.toList) mpis- pretty (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 recipientKeyIdentifier pka esk))) =- pretty "PKESK v6:" <+>- pretty "recipient key identifier" <+>- pretty (bsToHexUpper recipientKeyIdentifier) <+>- pretty pka <+> pretty (bsToHexUpper esk)- pretty (SignaturePkt sp) = pretty sp- pretty (SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k mbs))) =- pretty "SKESK v4:" <+>- pretty sa <+> pretty s2k <+> pretty (fmap bsToHexUpper mbs)- pretty (SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 sa aa s2k iv esk tag))) =- pretty "SKESK v6:" <+>- pretty sa <+>- pretty aa <+>- pretty s2k <+>- pretty (bsToHexUpper iv) <+>- pretty (bsToHexUpper esk) <+>- pretty (bsToHexUpper tag)- pretty (OnePassSignaturePkt (OPSPayloadV3Packet (OPSPayloadV3 pv st ha pka eoki nestedflag))) =- pretty "one-pass signature v" <> pretty pv <> pretty ':' <+>- pretty st <+> pretty ha <+> pretty pka <+> pretty eoki <+> pretty nestedflag- pretty (OnePassSignaturePkt (OPSPayloadV6Packet (OPSPayloadV6 st ha pka salt signerFingerprint nestedflag))) =- pretty "one-pass signature v6:" <+>- pretty st <+>- pretty ha <+>- pretty pka <+> pretty salt <+> pretty (bsToHexUpper signerFingerprint) <+>- pretty nestedflag- pretty (SecretKeyPkt pkp ska) =- pretty "secret key:" <+> pretty pkp <+> pretty ska- pretty (PublicKeyPkt pkp) = pretty "public key:" <+> pretty pkp- pretty (SecretSubkeyPkt pkp ska) =- pretty "secret subkey:" <+> pretty pkp <+> pretty ska- pretty (CompressedDataPkt ca cdp) =- pretty "compressed-data:" <+> pretty ca <+> prettyLBS cdp- pretty (SymEncDataPkt bs) =- pretty "symmetrically-encrypted-data:" <+> pretty (bsToHexUpper bs)- pretty (MarkerPkt bs) = pretty "marker:" <+> pretty (bsToHexUpper bs)- pretty (LiteralDataPkt dt fn ts bs) =- pretty "literal-data" <+>- pretty dt <+> prettyLBS fn <+> pretty ts <+> pretty (bsToHexUpper bs)- pretty (TrustPkt bs) = pretty "trust:" <+> pretty (BL.unpack bs)- pretty (UserIdPkt u) = pretty "user-ID:" <+> pretty u- pretty (PublicSubkeyPkt pkp) = pretty "public subkey:" <+> pretty pkp- pretty (UserAttributePkt us) = pretty "user-attribute:" <+> pretty us- pretty (SymEncIntegrityProtectedDataPkt (SEIPD1 pv bs)) =- pretty "symmetrically-encrypted-integrity-protected-data v" <> pretty pv <>- pretty ':' <+>- pretty (bsToHexUpper bs)- pretty (SymEncIntegrityProtectedDataPkt (SEIPD2 sa aa chunkSize salt bs)) =- pretty "symmetrically-encrypted-integrity-protected-data v2:" <+>- pretty sa <+> pretty aa <+> pretty chunkSize <+> pretty salt <+>- pretty (bsToHexUpper bs)- pretty (ModificationDetectionCodePkt bs) =- pretty "MDC:" <+> pretty (bsToHexUpper bs)- pretty (OtherPacketPkt t bs) =- pretty "unknown packet type" <+>- pretty t <> pretty ':' <+> pretty (bsToHexUpper bs)- pretty (BrokenPacketPkt s t bs) =- pretty "BROKEN packet (" <> pretty s <> pretty ')' <+>- pretty t <> pretty ':' <+> pretty (bsToHexUpper bs)+ pretty (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 pv eoki pka mpis))) =+ pretty "PKESK v"+ <> pretty pv+ <> pretty ':'+ <+> pretty eoki+ <+> pretty pka+ <+> (pretty . NE.toList) mpis+ pretty+ ( PKESKPkt+ ( PKESKPayloadV6Packet+ (PKESKPayloadV6 recipientKeyIdentifier pka esk)+ )+ ) =+ pretty "PKESK v6:"+ <+> pretty "recipient key identifier"+ <+> pretty (bsToHexUpper recipientKeyIdentifier)+ <+> pretty pka+ <+> pretty (bsToHexUpper esk)+ pretty (SignaturePkt sp) = pretty sp+ pretty (SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k mbs))) =+ pretty "SKESK v4:"+ <+> pretty sa+ <+> pretty s2k+ <+> pretty (fmap bsToHexUpper mbs)+ pretty+ ( SKESKPkt+ (SKESKPayloadV6Packet (SKESKPayloadV6 sa aa s2k iv esk tag))+ ) =+ pretty "SKESK v6:"+ <+> pretty sa+ <+> pretty aa+ <+> pretty s2k+ <+> pretty (bsToHexUpper iv)+ <+> pretty (bsToHexUpper esk)+ <+> pretty (bsToHexUpper tag)+ pretty+ ( OnePassSignaturePkt+ (OPSPayloadV3Packet (OPSPayloadV3 pv st ha pka eoki nestedflag))+ ) =+ pretty "one-pass signature v"+ <> pretty pv+ <> pretty ':'+ <+> pretty st+ <+> pretty ha+ <+> pretty pka+ <+> pretty eoki+ <+> pretty nestedflag+ pretty+ ( OnePassSignaturePkt+ ( OPSPayloadV6Packet+ (OPSPayloadV6 st ha pka salt signerFingerprint nestedflag)+ )+ ) =+ pretty "one-pass signature v6:"+ <+> pretty st+ <+> pretty ha+ <+> pretty pka+ <+> pretty salt+ <+> pretty (bsToHexUpper signerFingerprint)+ <+> pretty nestedflag+ pretty (SecretKeyPkt pkp ska) =+ pretty "secret key:" <+> pretty pkp <+> pretty ska+ pretty (PublicKeyPkt pkp) = pretty "public key:" <+> pretty pkp+ pretty (SecretSubkeyPkt pkp ska) =+ pretty "secret subkey:" <+> pretty pkp <+> pretty ska+ pretty (CompressedDataPkt ca cdp) =+ pretty "compressed-data:" <+> pretty ca <+> prettyLBS cdp+ pretty (SymEncDataPkt bs) =+ pretty "symmetrically-encrypted-data:"+ <+> pretty (bsToHexUpper bs)+ pretty (MarkerPkt bs) = pretty "marker:" <+> pretty (bsToHexUpper bs)+ pretty (LiteralDataPkt dt fn ts bs) =+ pretty "literal-data"+ <+> pretty dt+ <+> prettyLBS fn+ <+> pretty ts+ <+> pretty (bsToHexUpper bs)+ pretty (TrustPkt bs) = pretty "trust:" <+> pretty (BL.unpack bs)+ pretty (UserIdPkt u) = pretty "user-ID:" <+> pretty u+ pretty (PublicSubkeyPkt pkp) = pretty "public subkey:" <+> pretty pkp+ pretty (UserAttributePkt us) = pretty "user-attribute:" <+> pretty us+ pretty (SymEncIntegrityProtectedDataPkt (SEIPD1 pv bs)) =+ pretty "symmetrically-encrypted-integrity-protected-data v"+ <> pretty pv+ <> pretty ':'+ <+> pretty (bsToHexUpper bs)+ pretty (SymEncIntegrityProtectedDataPkt (SEIPD2 sa aa chunkSize salt bs)) =+ pretty "symmetrically-encrypted-integrity-protected-data v2:"+ <+> pretty sa+ <+> pretty aa+ <+> pretty chunkSize+ <+> pretty salt+ <+> pretty (bsToHexUpper bs)+ pretty (ModificationDetectionCodePkt bs) =+ pretty "MDC:" <+> pretty (bsToHexUpper bs)+ pretty (PaddingPkt bs) =+ pretty "Padding:" <+> pretty (bsToHexUpper bs)+ pretty (OtherPacketPkt t bs) =+ pretty "unknown packet type"+ <+> pretty t+ <> pretty ':'+ <+> pretty (bsToHexUpper bs)+ pretty (BrokenPacketPkt s t bs) =+ pretty "BROKEN packet ("+ <> pretty s+ <> pretty ')'+ <+> pretty t+ <> pretty ':'+ <+> pretty (bsToHexUpper bs) instance A.ToJSON Pkt where- toJSON (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 pv eoki pka mpis))) =- object- [ key "pkesk" .=+ toJSON (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 pv eoki pka mpis))) = object- [ key "version" .= pv- , key "keyid" .= eoki- , key "pkalgo" .= pka- , key "mpis" .= NE.toList mpis- ]- ]- toJSON (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 recipientKeyIdentifier pka esk))) =- object- [ key "pkesk_v6" .=+ [ AK.fromString "pkesk"+ .= object+ [ AK.fromString "version" .= pv+ , AK.fromString "keyid" .= eoki+ , AK.fromString "pkalgo" .= pka+ , AK.fromString "mpis" .= NE.toList mpis+ ]+ ]+ toJSON+ ( PKESKPkt+ ( PKESKPayloadV6Packet+ (PKESKPayloadV6 recipientKeyIdentifier pka esk)+ )+ ) =+ object+ [ AK.fromString "pkesk"+ .= object+ [ AK.fromString "version" .= (6 :: PacketVersion)+ , AK.fromString "recipient_key_identifier"+ .= BL.unpack recipientKeyIdentifier+ , AK.fromString "pkalgo" .= pka+ , AK.fromString "esk" .= BL.unpack esk+ ]+ ]+ toJSON (SignaturePkt sp) = object [AK.fromString "signature" .= sp]+ toJSON (SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k mbs))) = object- [ key "recipient_key_identifier" .= BL.unpack recipientKeyIdentifier- , key "pkalgo" .= pka- , key "esk" .= BL.unpack esk- ]- ]- toJSON (SignaturePkt sp) = object [key "signature" .= sp]- toJSON (SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k mbs))) =- object- [ key "skesk" .=+ [ AK.fromString "skesk"+ .= object+ [ AK.fromString "version" .= (4 :: PacketVersion)+ , AK.fromString "symalgo" .= sa+ , AK.fromString "s2k" .= s2k+ , AK.fromString "data" .= maybe mempty BL.unpack mbs+ ]+ ]+ toJSON+ ( SKESKPkt+ (SKESKPayloadV6Packet (SKESKPayloadV6 sa aa s2k iv esk tag))+ ) =+ object+ [ AK.fromString "skesk"+ .= object+ [ AK.fromString "version" .= (6 :: PacketVersion)+ , AK.fromString "symalgo" .= sa+ , AK.fromString "aead" .= aa+ , AK.fromString "s2k" .= s2k+ , AK.fromString "iv" .= BL.unpack iv+ , AK.fromString "esk" .= BL.unpack esk+ , AK.fromString "tag" .= BL.unpack tag+ ]+ ]+ toJSON+ ( OnePassSignaturePkt+ (OPSPayloadV3Packet (OPSPayloadV3 pv st ha pka eoki nestedflag))+ ) =+ object+ [ AK.fromString "onepasssignature"+ .= object+ [ AK.fromString "version" .= pv+ , AK.fromString "sigtype" .= st+ , AK.fromString "hashalgo" .= ha+ , AK.fromString "pkalgo" .= pka+ , AK.fromString "keyid" .= eoki+ , AK.fromString "nested" .= nestedflag+ ]+ ]+ toJSON+ ( OnePassSignaturePkt+ ( OPSPayloadV6Packet+ (OPSPayloadV6 st ha pka salt signerFingerprint nestedflag)+ )+ ) =+ object+ [ AK.fromString "onepasssignature"+ .= object+ [ AK.fromString "version" .= (6 :: Word8)+ , AK.fromString "sigtype" .= st+ , AK.fromString "hashalgo" .= ha+ , AK.fromString "pkalgo" .= pka+ , AK.fromString "salt" .= salt+ , AK.fromString "fingerprint" .= BL.unpack signerFingerprint+ , AK.fromString "nested" .= nestedflag+ ]+ ]+ toJSON (SecretKeyPkt pkp ska) = object- [ key "version" .= (4 :: PacketVersion)- , key "symalgo" .= sa- , key "s2k" .= s2k- , key "data" .= maybe mempty BL.unpack mbs- ]- ]- toJSON (SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 sa aa s2k iv esk tag))) =- object- [ key "skesk-v6" .=+ [ AK.fromString "secretkey"+ .= object+ [AK.fromString "public" .= pkp, AK.fromString "secret" .= ska]+ ]+ toJSON (PublicKeyPkt pkp) = object [AK.fromString "publickey" .= pkp]+ toJSON (SecretSubkeyPkt pkp ska) = object- [ key "version" .= (6 :: PacketVersion)- , key "symalgo" .= sa- , key "aead" .= aa- , key "s2k" .= s2k- , key "iv" .= BL.unpack iv- , key "esk" .= BL.unpack esk- , key "tag" .= BL.unpack tag- ]- ]- toJSON (OnePassSignaturePkt (OPSPayloadV3Packet (OPSPayloadV3 pv st ha pka eoki nestedflag))) =- object- [ key "onepasssignature" .=+ [ AK.fromString "secretsubkey"+ .= object+ [AK.fromString "public" .= pkp, AK.fromString "secret" .= ska]+ ]+ toJSON (CompressedDataPkt ca cdp) = object- [ key "version" .= pv- , key "sigtype" .= st- , key "hashalgo" .= ha- , key "pkalgo" .= pka- , key "keyid" .= eoki- , key "nested" .= nestedflag- ]- ]- toJSON (OnePassSignaturePkt (OPSPayloadV6Packet (OPSPayloadV6 st ha pka salt signerFingerprint nestedflag))) =- object- [ key "onepasssignature_v6" .=+ [ AK.fromString "compresseddata"+ .= object+ [ AK.fromString "compressionalgo" .= ca+ , AK.fromString "data" .= BL.unpack cdp+ ]+ ]+ toJSON (SymEncDataPkt bs) = object [AK.fromString "symencdata" .= BL.unpack bs]+ toJSON (MarkerPkt bs) = object [AK.fromString "marker" .= BL.unpack bs]+ toJSON (LiteralDataPkt dt fn ts bs) = object- [ key "version" .= (6 :: Word8)- , key "sigtype" .= st- , key "hashalgo" .= ha- , key "pkalgo" .= pka- , key "salt" .= salt- , key "fingerprint" .= BL.unpack signerFingerprint- , key "nested" .= nestedflag- ]- ]- toJSON (SecretKeyPkt pkp ska) =- object- [ key "secretkey" .=- object [key "public" .= pkp, key "secret" .= ska]- ]- toJSON (PublicKeyPkt pkp) = object [key "publickey" .= pkp]- toJSON (SecretSubkeyPkt pkp ska) =- object- [ key "secretsubkey" .=- object [key "public" .= pkp, key "secret" .= ska]- ]- toJSON (CompressedDataPkt ca cdp) =- object- [ key "compresseddata" .=- object [key "compressionalgo" .= ca, key "data" .= BL.unpack cdp]- ]- toJSON (SymEncDataPkt bs) = object [key "symencdata" .= BL.unpack bs]- toJSON (MarkerPkt bs) = object [key "marker" .= BL.unpack bs]- toJSON (LiteralDataPkt dt fn ts bs) =- object- [ key "literaldata" .=+ [ AK.fromString "literaldata"+ .= object+ [ AK.fromString "dt" .= dt+ , AK.fromString "filename" .= BL.unpack fn+ , AK.fromString "ts" .= ts+ , AK.fromString "data" .= BL.unpack bs+ ]+ ]+ toJSON (TrustPkt bs) = object [AK.fromString "trust" .= BL.unpack bs]+ toJSON (UserIdPkt u) = object [AK.fromString "userid" .= u]+ toJSON (PublicSubkeyPkt pkp) = object [AK.fromString "publicsubkkey" .= pkp]+ toJSON (UserAttributePkt us) = object [AK.fromString "userattribute" .= us]+ toJSON (SymEncIntegrityProtectedDataPkt (SEIPD1 pv bs)) = object- [ key "dt" .= dt- , key "filename" .= BL.unpack fn- , key "ts" .= ts- , key "data" .= BL.unpack bs- ]- ]- toJSON (TrustPkt bs) = object [key "trust" .= BL.unpack bs]- toJSON (UserIdPkt u) = object [key "userid" .= u]- toJSON (PublicSubkeyPkt pkp) = object [key "publicsubkkey" .= pkp]- toJSON (UserAttributePkt us) = object [key "userattribute" .= us]- toJSON (SymEncIntegrityProtectedDataPkt (SEIPD1 pv bs)) =- object- [ key "symencipd" .=- object [key "version" .= pv, key "data" .= BL.unpack bs]- ]- toJSON (SymEncIntegrityProtectedDataPkt (SEIPD2 sa aa chunkSize salt bs)) =- object- [ key "symencipd_v2" .=+ [ AK.fromString "symencipd"+ .= object+ [ AK.fromString "version" .= pv+ , AK.fromString "data" .= BL.unpack bs+ ]+ ]+ toJSON (SymEncIntegrityProtectedDataPkt (SEIPD2 sa aa chunkSize salt bs)) = object- [ key "symalgo" .= sa- , key "aeadalgo" .= aa- , key "chunksize" .= chunkSize- , key "salt" .= salt- , key "data" .= BL.unpack bs- ]- ]- toJSON (ModificationDetectionCodePkt bs) =- object [key "mdc" .= BL.unpack bs]- toJSON (OtherPacketPkt t bs) =- object- [ key "otherpacket" .=- object [key "tag" .= t, key "data" .= BL.unpack bs]- ]- toJSON (BrokenPacketPkt s t bs) =- object- [ key "brokenpacket" .=+ [ AK.fromString "symencipd"+ .= object+ [ AK.fromString "version" .= (2 :: PacketVersion)+ , AK.fromString "symalgo" .= sa+ , AK.fromString "aeadalgo" .= aa+ , AK.fromString "chunksize" .= chunkSize+ , AK.fromString "salt" .= salt+ , AK.fromString "data" .= BL.unpack bs+ ]+ ]+ toJSON (ModificationDetectionCodePkt bs) =+ object [AK.fromString "mdc" .= BL.unpack bs]+ toJSON (PaddingPkt bs) =+ object [AK.fromString "padding" .= BL.unpack bs]+ toJSON (OtherPacketPkt t bs) = object- [ key "error" .= s- , key "tag" .= t- , key "data" .= BL.unpack bs- ]- ]+ [ AK.fromString "otherpacket"+ .= object+ [AK.fromString "tag" .= t, AK.fromString "data" .= BL.unpack bs]+ ]+ toJSON (BrokenPacketPkt s t bs) =+ object+ [ AK.fromString "brokenpacket"+ .= object+ [ AK.fromString "error" .= s+ , AK.fromString "tag" .= t+ , AK.fromString "data" .= BL.unpack bs+ ]+ ] pktTag :: Pkt -> Word8 pktTag PKESKPkt {} = 1@@ -485,16 +586,17 @@ pktTag (UserAttributePkt _) = 17 pktTag SymEncIntegrityProtectedDataPkt {} = 18 pktTag (ModificationDetectionCodePkt _) = 19+pktTag (PaddingPkt _) = 21 pktTag (OtherPacketPkt t _) = t pktTag (BrokenPacketPkt _ t _) = t -- is this the right thing to do? renderKeyPktCoercionError :: KeyPktCoercionError -> String renderKeyPktCoercionError (NotAKeyPacket pkt) =- "Expected a key packet, got tag " ++ show (pktTag pkt)+ "Expected a key packet, got tag " ++ show (pktTag pkt) renderKeyPktCoercionError (ExpectedPublicKeyPacket pkt) =- "Expected a public key packet, got tag " ++ show (pktTag pkt)+ "Expected a public key packet, got tag " ++ show (pktTag pkt) renderKeyPktCoercionError (ExpectedSecretKeyPacket pkt) =- "Expected a secret key packet, got tag " ++ show (pktTag pkt)+ "Expected a secret key packet, got tag " ++ show (pktTag pkt) keyPktRole :: KeyPkt k -> KeyPktRole keyPktRole KeyPktPublicPrimary {} = KeyPktPrimary@@ -521,7 +623,8 @@ secretKeyPktSKAddendum (KeyPktSecretPrimary _ ska) = ska secretKeyPktSKAddendum (KeyPktSecretSubkey _ ska) = ska -mkPrimaryKeyPkt :: SomePKPayload -> Maybe SKAddendum -> SomeKeyPkt+mkPrimaryKeyPkt+ :: SomePKPayload -> Maybe SKAddendum -> SomeKeyPkt mkPrimaryKeyPkt pkp Nothing = SomeKeyPkt (KeyPktPublicPrimary pkp) mkPrimaryKeyPkt pkp (Just ska) = SomeKeyPkt (KeyPktSecretPrimary pkp ska) @@ -542,7 +645,8 @@ someKeyPktToPkt :: SomeKeyPkt -> Pkt someKeyPktToPkt (SomeKeyPkt keyPkt) = keyPktToPkt keyPkt -pktToSomeKeyPktEither :: Pkt -> Either KeyPktCoercionError SomeKeyPkt+pktToSomeKeyPktEither+ :: Pkt -> Either KeyPktCoercionError SomeKeyPkt pktToSomeKeyPktEither (PublicKeyPkt pkp) = Right (SomeKeyPkt (KeyPktPublicPrimary pkp)) pktToSomeKeyPktEither (PublicSubkeyPkt pkp) = Right (SomeKeyPkt (KeyPktPublicSubkey pkp)) pktToSomeKeyPktEither (SecretKeyPkt pkp ska) = Right (SomeKeyPkt (KeyPktSecretPrimary pkp ska))@@ -552,7 +656,8 @@ pktToSomeKeyPkt :: Pkt -> Maybe SomeKeyPkt pktToSomeKeyPkt = either (const Nothing) Just . pktToSomeKeyPktEither -pktToPublicKeyPktEither :: Pkt -> Either KeyPktCoercionError (KeyPkt 'PublicPkt)+pktToPublicKeyPktEither+ :: Pkt -> Either KeyPktCoercionError (KeyPkt 'PublicPkt) pktToPublicKeyPktEither (PublicKeyPkt pkp) = Right (KeyPktPublicPrimary pkp) pktToPublicKeyPktEither (PublicSubkeyPkt pkp) = Right (KeyPktPublicSubkey pkp) pktToPublicKeyPktEither pkt = Left (ExpectedPublicKeyPacket pkt)@@ -560,7 +665,8 @@ pktToPublicKeyPkt :: Pkt -> Maybe (KeyPkt 'PublicPkt) pktToPublicKeyPkt = either (const Nothing) Just . pktToPublicKeyPktEither -pktToSecretKeyPktEither :: Pkt -> Either KeyPktCoercionError (KeyPkt 'SecretPkt)+pktToSecretKeyPktEither+ :: Pkt -> Either KeyPktCoercionError (KeyPkt 'SecretPkt) pktToSecretKeyPktEither (SecretKeyPkt pkp ska) = Right (KeyPktSecretPrimary pkp ska) pktToSecretKeyPktEither (SecretSubkeyPkt pkp ska) = Right (KeyPktSecretSubkey pkp ska) pktToSecretKeyPktEither pkt = Left (ExpectedSecretKeyPacket pkt)@@ -568,25 +674,36 @@ pktToSecretKeyPkt :: Pkt -> Maybe (KeyPkt 'SecretPkt) pktToSecretKeyPkt = either (const Nothing) Just . pktToSecretKeyPktEither --- | Convert secret key/subkey packets to their public-key packet forms.--- Non-secret packets are returned unchanged.+{- | Convert secret key/subkey packets to their public-key packet forms.+Non-secret packets are returned unchanged.+-} publicKeyPacketOf :: Pkt -> Pkt publicKeyPacketOf pkt =- maybe pkt (keyPktToPkt . keyPktToPublicView) (pktToSecretKeyPkt pkt)+ maybe+ pkt+ (keyPktToPkt . keyPktToPublicView)+ (pktToSecretKeyPkt pkt) -data Verification =- Verification+data Verification+ = Verification { _verificationSigner :: SomePKPayload , _verificationSignature :: SignaturePayload , _verificationWarnings :: [VerificationWarning] } data VerificationWarning- = MissingSubkeyBackSignatureWarning- deriving (Eq, Show)+ = MissingSubkeyBackSignatureWarning+ | DeprecatedHashAlgorithmWarning HashAlgorithm+ | UnsupportedHashAlgorithmWarning HashAlgorithm+ | PkaMismatchWarning PubKeyAlgorithm PubKeyAlgorithm+ | UnsupportedCriticalSubpacketWarning SigType+ | LegacyIssuerKeyIdInV6Warning+ | InvalidSignatureContextWarning SigType+ | ExpiredSignatureWarning+ deriving (Eq, Show) -data SOPVVerification =- SOPVVerification+data SOPVVerification+ = SOPVVerification { _sopvvDateStamp :: UTCTime , _sopvvFingerprint :: Fingerprint , _sopvvPrimaryFingerprint :: Fingerprint
Data/Conduit/OpenPGP/Decrypt.hs view
@@ -2,2483 +2,2929 @@ -- Copyright © 2013-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).--{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PackageImports #-}-{-# LANGUAGE TypeApplications #-}--module Data.Conduit.OpenPGP.Decrypt- ( conduitDecrypt- , conduitDecryptWithReport- , DecryptOptions(..)- , DecryptKeyResolution(..)- , PKESKRecipientKey(..)- , PKESKAttemptFailureKind(..)- , PKESKAttemptFailure(..)- , DecryptOutcome(..)- , DecryptReport(..)- , DecryptSessionKeyResolutionReport(..)- , DecryptSessionKeyResolutionPath(..)- , PKESKResolverAttempt(..)- , PKESKResolverAttemptAction(..)- , decryptSEIPDv2Payload- ) where--import Codec.Encryption.OpenPGP.BlockCipher (renderCipherError, keySize)-import Control.Exception (SomeException, displayException, try)-import Control.Applicative ((<|>))-import Control.Monad (when)-import Control.Monad.IO.Class (MonadIO(..))-import Control.Monad.IO.Unlift (MonadUnliftIO)-import Control.Monad.Trans.Class (lift)-import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT)-import Control.Monad.Trans.Resource (MonadResource, MonadThrow)-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 Crypto.Number.Serialize (i2osp, os2ip)-import qualified Crypto.PubKey.Curve25519 as C25519-import qualified Crypto.PubKey.Curve448 as C448-import qualified Crypto.PubKey.ECC.DH as ECCDH-import qualified Crypto.PubKey.ECC.ECDSA as ECDSA-import qualified Crypto.PubKey.ECC.Types as ECCT-import qualified Crypto.PubKey.RSA.PKCS15 as P15-import qualified Crypto.PubKey.RSA.Types as RSATypes-import Data.Binary (get)-import Data.Binary.Put (putWord64be, runPut)-import Data.Bits (countLeadingZeros, shiftL, shiftR, xor)-import Data.Bifunctor (first)-import qualified Data.ByteArray as BA-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import Data.Conduit-import qualified Data.Conduit.Binary as CB-import qualified Data.Conduit.List as CL-import Data.Conduit.OpenPGP.Compression (conduitDecompress)-import Data.Conduit.Serialization.Binary (conduitGet)-import Data.IORef (IORef, modifyIORef', newIORef, readIORef, writeIORef)-import Data.List (intercalate, nub)-import Data.List.NonEmpty (NonEmpty(..))-import Data.Maybe (catMaybes, isNothing, mapMaybe)-import Data.Word (Word16, Word8, Word64)--import Codec.Encryption.OpenPGP.CFB (decryptOpenPGPCfb, decryptPreservingNonce, validateSEIPD1MDC, calculateMDC)-import Codec.Encryption.OpenPGP.Internal (leftPadTo)-import Codec.Encryption.OpenPGP.Internal.CryptoAES (withAESCipher)-import Codec.Encryption.OpenPGP.Internal.CryptoECDH- ( normalizeMontgomeryPublic- , buildECDHKDFParam- , deriveECDHKek- )-import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)-import Codec.Encryption.OpenPGP.Internal.CryptoSEIPDv2- ( aeadModeAndNonceSizeForSEIPDv2- , decryptSKESK6SessionKey- , deriveSKESK6KEK- , seipdv2SymmetricKeySize- )-import Codec.Encryption.OpenPGP.Policy- ( DecryptPolicy(..)- , defaultDecryptPolicy- , validateTable30PolicyForRecipient- )-import Codec.Encryption.OpenPGP.Internal.RFC7253OCB (decryptWithOCBRFC7253With)-import Codec.Encryption.OpenPGP.S2K- ( decodeOpenPGPEncodedSessionKey- , renderEncodedSessionKeyError- , renderS2KError- , S2KError(..)- , skesk2Key- , skesk2SessionKey- , string2Key- )-import Codec.Encryption.OpenPGP.Types-import Codec.Encryption.OpenPGP.SecretKey (decryptPrivateKey)-import Control.Lens ((.~), ix)-import Data.Conduit.OpenPGP.Keyring.Instances ()-import qualified Data.IxSet.Typed as IxSet--data RecursorState =- RecursorState- { _depth :: Int- , _pendingESKs :: [PendingESK]- , _lastNonce :: Maybe B.ByteString- , _lastClearText :: Maybe B.ByteString- , _decryptPolicy :: DecryptPolicy- }- deriving (Eq, Show)--def :: RecursorState-def = RecursorState 0 [] Nothing Nothing defaultDecryptPolicy--data DecryptStreamPhase- = ActiveDecryptPhase- | FinishedDecryptPhase- | MalformedDecryptPhase--data DecryptStreamState (phase :: DecryptStreamPhase) where- ActiveDecryptState ::- RecursorState- -> DecryptStreamState 'ActiveDecryptPhase- FinishedDecryptState ::- RecursorState- -> Bool- -> DecryptStreamState 'FinishedDecryptPhase- MalformedDecryptState ::- RecursorState- -> String- -> DecryptStreamState 'MalformedDecryptPhase--data SomeDecryptStreamState where- SomeDecryptStreamState ::- DecryptStreamState phase- -> SomeDecryptStreamState--data PendingESK- = PendingPKESK PKESKPayload- | PendingSKESK SKESKPayload- deriving (Eq, Show)--data EncryptedPayloadVersion- = LegacyEncryptedPayloadVersion- | SEIPDv2EncryptedPayloadVersion--data EncryptedPayloadFlavor (v :: EncryptedPayloadVersion) where- LegacyEncryptedPayload :: EncryptedPayloadFlavor 'LegacyEncryptedPayloadVersion- SEIPDv2EncryptedPayload ::- SymmetricAlgorithm- -> AEADAlgorithm- -> EncryptedPayloadFlavor 'SEIPDv2EncryptedPayloadVersion--type InputCallback m = String -> m BL.ByteString--data PKESKRecipientKey =- PKESKRecipientKey- { pkeskRecipientPKPayload :: Maybe SomePKPayload- -- ^ Public key payload for the recipient. Required for X25519, X448,- -- and ECDH unwrap paths; may be 'Nothing' for RSA.- , pkeskRecipientSKey :: SKey- -- ^ Corresponding secret key.- }--data PKESKAttemptFailure =- PKESKAttemptFailure- { pkeskAttemptFailureKeyContext :: Maybe (KeyVersion, PubKeyAlgorithm)- , pkeskAttemptFailureKind :: PKESKAttemptFailureKind- , pkeskAttemptFailureReason :: String- }- deriving (Eq, Show)--data PKESKAttemptFailureKind- = PKESKAttemptUnwrapFailed- | PKESKAttemptSessionMaterialDecodeFailed- deriving (Eq, Show)--data PKESKResolverError- = ResolverPolicyDenied String- | ResolverBackendUnavailable String- | ResolverInvalidResponse String- deriving (Eq, Show)--data PKESKResolveRequest =- PKESKResolveRequest- { reqPKESK :: PKESKPayload- , reqProbePacket :: Pkt- , reqIsWildcardRecipient :: Bool- , reqAttemptIndex :: Int- , reqPreviousFailures :: [PKESKAttemptFailure]- }- deriving (Eq, Show)--data PKESKResolveAction- = ResolveWith PKESKRecipientKey- | ResolveSkip- | ResolveExhausted- | ResolveFail PKESKResolverError--type PKESKResolver m = PKESKResolveRequest -> m PKESKResolveAction---- | Build a stateful 'PKESKResolver' that iterates over a pre-populated--- candidate list. The returned resolver yields candidates in order and--- returns 'ResolveExhausted' once the list is depleted.--- | How to resolve secret keys for PKESK-encrypted messages.-data DecryptKeyResolution- = DecryptWithoutPKESK- -- ^ Do not attempt PKESK decryption; fall through to manual session-key- -- input via the passphrase callback instead.- | DecryptWithKeyring SecretKeyring- -- ^ Resolve secret keys from a 'SecretKeyring'. Only unencrypted secret- -- keys (not passphrase-protected) are used. For passphrase-protected keys,- -- use 'DecryptWithKeyringAndPassphrase'.- | DecryptWithKeyringAndPassphrase SecretKeyring (SomePKPayload -> IO (Maybe BL.ByteString))- -- ^ Resolve secret keys from a 'SecretKeyring', unlocking passphrase-- -- protected keys using the provided callback. The callback receives the- -- public key payload and returns the passphrase, or 'Nothing' to skip.- | DecryptWithUnwrapCandidatesCallback (KeyIdentifier -> PubKeyAlgorithm -> IO [PKESKRecipientKey])- -- ^ Preferred callback form for non-keyring key material. The callback- -- receives a typed key identifier (8-octet key ID, fingerprint, or- -- wildcard) plus the packet public-key algorithm, then returns all- -- matching candidates in priority order. hOpenPGP iterates candidates- -- deterministically without re-calling the callback for each retry.---- | Canonical decrypt configuration.------ Most callers should prefer 'conduitDecrypt' and set:------ * 'decryptOptionsKeyResolution' to 'DecryptWithKeyring' or 'DecryptWithKeyringAndPassphrase'--- * 'decryptOptionsPolicy' to strict ('defaultDecryptPolicy') or lenient--- * 'decryptOptionsPassphraseCallback' for SKESK passphrase lookup-data DecryptOptions =- DecryptOptions- { decryptOptionsKeyResolution :: DecryptKeyResolution- , decryptOptionsPolicy :: DecryptPolicy- , decryptOptionsPassphraseCallback :: InputCallback IO- }---- | Outcome of a checked decrypt conduit run.------ Use 'conduitDecrypt' to obtain this value. Because--- 'Data.Conduit..|' preserves the--- /rightmost/ conduit's return value, callers who also need the decrypted--- packet stream should use 'Data.Conduit.fuseBoth':------ @--- (outcome, pkts) \<- runConduit $ source .| fuseBoth (conduitDecrypt opts) CL.consume--- @-data DecryptOutcome- = DecryptClean- -- ^ The integrity-terminating marker (MDC or SEIPD v2 final AEAD tag)- -- was seen and no further packets arrived. The message was- -- well-formed end-to-end.- | DecryptTruncated- -- ^ The input stream ended before any integrity-terminating marker was- -- seen. The ciphertext was incomplete.- | DecryptTrailingData- -- ^ An integrity-terminating marker was seen, but additional packets- -- followed it. Only possible with 'lenientDecryptPolicy' (strict- -- policy reports 'DecryptMalformedStructure' instead). The trailing- -- packets were forwarded downstream unchanged.- | DecryptMalformedStructure String- -- ^ A structural packet-sequencing violation was detected. The- -- 'String' describes the specific violation:- --- -- * PKESK version does not match the SEIPD version (e.g. a v6 PKESK- -- preceding a SEIPDv1 payload, or a v4 SKESK preceding a SEIPDv2- -- payload).- --- -- * ESK packets arrived in the wrong order relative to the encrypted- -- data packet (e.g. a literal-data packet appeared between a PKESK- -- and the SEIPD it was intended to protect).- --- -- * A packet arrived after the message integrity boundary (trailing- -- data). Under 'defaultDecryptPolicy' this is reported here; under- -- 'lenientDecryptPolicy' it is reported as 'DecryptTrailingData'- -- instead.- deriving (Eq, Show)--data DecryptSessionKeyResolutionPath- = DecryptResolvedViaSKESK- | DecryptResolvedViaPKESK- | DecryptResolvedViaManualPKESKInput- deriving (Eq, Show)--data PKESKResolverAttemptAction- = ResolverAttemptResolveWith (Maybe (KeyVersion, PubKeyAlgorithm))- | ResolverAttemptSkip- | ResolverAttemptExhausted- | ResolverAttemptFail PKESKResolverError- deriving (Eq, Show)--data PKESKResolverAttempt =- PKESKResolverAttempt- { pkeskResolverAttemptPreviousFailures :: [PKESKAttemptFailure]- -- ^ Failures from earlier attempts on the same PKESK packet.- , pkeskResolverAttemptAction :: PKESKResolverAttemptAction- }- deriving (Eq, Show)--data DecryptSessionKeyResolutionReport =- DecryptSessionKeyResolutionReport- { decryptSessionResolutionPath :: DecryptSessionKeyResolutionPath- , decryptSessionResolutionSKESKErrors :: [String]- , decryptSessionResolutionPKESKErrors :: [String]- , decryptSessionResolutionResolverAttempts :: [PKESKResolverAttempt]- }- deriving (Eq, Show)--data DecryptReport =- DecryptReport- { decryptReportOutcome :: DecryptOutcome- , decryptReportSessionKeyResolutions :: [DecryptSessionKeyResolutionReport]- }- deriving (Eq, Show)---- | AEAD decryption context (Reader monad eliminates parameter threading)-data AEADDecryptContext cipher =- AEADDecryptContext- { aeadMode :: CCT.AEADMode- , aeadInfo :: B.ByteString- , aeadChunkSize :: Word8- , aeadNoncePrefix :: B.ByteString- , aeadCipher :: cipher- }---- | ReaderT wrapper for AEAD decryption computations-type AEADDecrypt cipher = ReaderT (AEADDecryptContext cipher) (Either String)--conduitDecrypt ::- (MonadFail m, MonadUnliftIO m, MonadResource m, MonadThrow m)- => DecryptOptions- -> ConduitT Pkt Pkt m DecryptOutcome-conduitDecrypt opts =- decryptReportOutcome <$> conduitDecryptWithReport opts--conduitDecryptWithReport ::- (MonadFail m, MonadUnliftIO m, MonadResource m, MonadThrow m)- => DecryptOptions- -> ConduitT Pkt Pkt m DecryptReport-conduitDecryptWithReport opts = do- reportRef <- liftIO (newIORef [])- resolver <- liftIO (buildDecryptResolver (decryptOptionsKeyResolution opts))- let allowManualPKESKPrompt =- case decryptOptionsKeyResolution opts of- DecryptWithoutPKESK -> True- _ -> False- outcome <-- conduitDecryptChecked'- (def {_decryptPolicy = decryptOptionsPolicy opts})- allowManualPKESKPrompt- resolver- (decryptOptionsPassphraseCallback opts)- (Just reportRef)- resolutions <- reverse <$> liftIO (readIORef reportRef)- pure- DecryptReport- { decryptReportOutcome = outcome- , decryptReportSessionKeyResolutions = resolutions- }---- | Build an internal 'PKESKResolver' from the public 'DecryptKeyResolution'.-buildDecryptResolver :: DecryptKeyResolution -> IO (PKESKResolver IO)-buildDecryptResolver DecryptWithoutPKESK =- pure (\_ -> pure ResolveExhausted)-buildDecryptResolver (DecryptWithKeyring kr) =- buildKeyringResolver kr Nothing-buildDecryptResolver (DecryptWithKeyringAndPassphrase kr cb) =- buildKeyringResolver kr (Just cb)-buildDecryptResolver (DecryptWithUnwrapCandidatesCallback cb) =- buildUnwrapCandidatesResolver cb---- | Build a stateful resolver that looks up keys from a 'SecretKeyring'.-buildKeyringResolver ::- SecretKeyring- -> Maybe (SomePKPayload -> IO (Maybe BL.ByteString))- -> IO (PKESKResolver IO)-buildKeyringResolver kr maybePassphraseCb = do- -- Tracks (last PKESK, remaining wildcard candidates once initialized).- stateRef <- newIORef (Nothing :: Maybe PKESKPayload, Nothing :: Maybe [PKESKRecipientKey])- pure $ \req -> do- let pkesk = reqPKESK req- probe = reqProbePacket req- (lastPKESK, wildcardState) <- readIORef stateRef- let freshPKESK = Just pkesk /= lastPKESK- when freshPKESK $ writeIORef stateRef (Just pkesk, Nothing)- let wc = if freshPKESK then Nothing else wildcardState- case extractProbeKeyIdentifier probe of- KeyIdentifierWildcard -> do- -- Wildcard probe: iterate all keys- candidates <- case wc of- Nothing -> keyringCandidates (IxSet.toList kr)- Just cs -> pure cs- case candidates of- [] -> do- writeIORef stateRef (Just pkesk, Just [])- pure ResolveExhausted- (rk:rest) -> do- writeIORef stateRef (Just pkesk, Just rest)- pure (ResolveWith rk)- keyIdentifier -> do- -- Exact probe: direct lookup by key ID or fingerprint- let matchingTKs = matchingTKsForRecipient keyIdentifier- priorFailures = length (reqPreviousFailures req)- candidates <- keyringCandidates matchingTKs- case drop priorFailures candidates of- [] -> pure ResolveSkip- (rk:_) -> pure (ResolveWith rk)- where- matchingTKsForRecipient :: KeyIdentifier -> [TK 'SecretTK]- matchingTKsForRecipient keyIdentifier =- case keyIdentifier of- KeyIdentifierWildcard -> IxSet.toList kr- KeyIdentifierEightOctet rid -> IxSet.toList (kr IxSet.@= rid)- KeyIdentifierFingerprint rid ->- let indexedMatches = IxSet.toList (kr IxSet.@= rid)- in if null indexedMatches- then filter (tkMatchesRecipientFingerprint rid) (IxSet.toList kr)- else indexedMatches-- tkMatchesRecipientFingerprint :: Fingerprint -> TK 'SecretTK -> Bool- tkMatchesRecipientFingerprint rid tk =- any- (keyPktMatchesRecipientFingerprint rid)- (_tkPrimaryKey tk : map fst (_tkSubs tk))-- keyPktMatchesRecipientFingerprint ::- Fingerprint -> KeyPkt 'SecretPkt -> Bool- keyPktMatchesRecipientFingerprint rid (KeyPktSecretPrimary pkp _) =- pkPayloadMatchesRecipientFingerprint rid pkp- keyPktMatchesRecipientFingerprint rid (KeyPktSecretSubkey pkp _) =- pkPayloadMatchesRecipientFingerprint rid pkp-- pkPayloadMatchesRecipientFingerprint :: Fingerprint -> SomePKPayload -> Bool- pkPayloadMatchesRecipientFingerprint rid pkp =- fingerprint pkp `elem` recipientFingerprintMatchVariants rid-- recipientFingerprintMatchVariants :: Fingerprint -> [Fingerprint]- recipientFingerprintMatchVariants (Fingerprint rid)- | BL.length rid == 20 = [Fingerprint rid, Fingerprint (BL.cons 0x04 rid)]- | BL.length rid == 21 && BL.head rid == 0x04 = [Fingerprint rid, Fingerprint (BL.tail rid)]- | BL.length rid == 32 = [Fingerprint rid, Fingerprint (BL.cons 0x06 rid)]- | BL.length rid == 33 && BL.head rid == 0x06 = [Fingerprint rid, Fingerprint (BL.tail rid)]- | otherwise = [Fingerprint rid]-- keyringCandidates :: [TK 'SecretTK] -> IO [PKESKRecipientKey]- keyringCandidates tks = concat <$> mapM tkCandidates tks-- tkCandidates :: TK 'SecretTK -> IO [PKESKRecipientKey]- tkCandidates tk =- fmap catMaybes . mapM resolveKeyPair $- _tkPrimaryKey tk : map fst (_tkSubs tk)-- resolveKeyPair :: KeyPkt 'SecretPkt -> IO (Maybe PKESKRecipientKey)- resolveKeyPair (KeyPktSecretPrimary pkp (SUUnencrypted sk _)) =- pure $ Just PKESKRecipientKey {pkeskRecipientPKPayload = Just pkp, pkeskRecipientSKey = sk}- resolveKeyPair (KeyPktSecretSubkey pkp (SUUnencrypted sk _)) =- pure $ Just PKESKRecipientKey {pkeskRecipientPKPayload = Just pkp, pkeskRecipientSKey = sk}- resolveKeyPair (KeyPktSecretPrimary pkp ska) = unlockProtected pkp ska- resolveKeyPair (KeyPktSecretSubkey pkp ska) = unlockProtected pkp ska-- unlockProtected :: SomePKPayload -> SKAddendum -> IO (Maybe PKESKRecipientKey)- unlockProtected pkp ska =- case maybePassphraseCb of- Nothing -> pure Nothing- Just passphraseCb -> do- mPassphrase <- passphraseCb pkp- case mPassphrase of- Nothing -> pure Nothing- Just passphrase ->- case decryptPrivateKey (pkp, ska) passphrase of- Left _ -> pure Nothing- Right (SUUnencrypted sk _) ->- pure $ Just PKESKRecipientKey {pkeskRecipientPKPayload = Just pkp, pkeskRecipientSKey = sk}- Right _ -> pure Nothing--buildUnwrapCandidatesResolver ::- (KeyIdentifier -> PubKeyAlgorithm -> IO [PKESKRecipientKey])- -> IO (PKESKResolver IO)-buildUnwrapCandidatesResolver cb = do- stateRef <- newIORef (Nothing :: Maybe PKESKPayload, [] :: [(Pkt, [PKESKRecipientKey])], [] :: [SKey])- pure $ \req -> do- let pkesk = reqPKESK req- probePkt = reqProbePacket req- (lastPKESK, probeState, seenSKeys) <- readIORef stateRef- let freshPKESK = Just pkesk /= lastPKESK- when freshPKESK $ writeIORef stateRef (Just pkesk, [], [])- let state0 = if freshPKESK then [] else probeState- seen0 = if freshPKESK then [] else seenSKeys- case lookup probePkt state0 of- Just (next:rest) -> do- writeIORef- stateRef- ( Just pkesk- , updateProbeState probePkt rest state0- , pkeskRecipientSKey next : seen0- )- pure (ResolveWith next)- Just [] ->- pure ResolveSkip- Nothing -> do- candidates <- filterFreshCandidates seen0 <$> callbackCandidates probePkt- case candidates of- [] -> do- writeIORef stateRef (Just pkesk, updateProbeState probePkt [] state0, seen0)- pure ResolveSkip- (next:rest) -> do- writeIORef- stateRef- ( Just pkesk- , updateProbeState probePkt rest state0- , pkeskRecipientSKey next : seen0- )- pure (ResolveWith next)- where- callbackCandidates probePkt =- cb (extractProbeKeyIdentifier probePkt) (extractProbePKA probePkt)-- updateProbeState probePkt remaining state0 =- (probePkt, remaining) : filter ((/= probePkt) . fst) state0-- filterFreshCandidates seenSKeys =- filter (\candidate -> pkeskRecipientSKey candidate `notElem` seenSKeys)---- | Extract the recipient identifier from a PKESK probe packet.-extractProbeKeyIdentifier :: Pkt -> KeyIdentifier-extractProbeKeyIdentifier (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ rid _ _)))- | isWildcardV3RecipientKeyId rid = KeyIdentifierWildcard- | otherwise = KeyIdentifierEightOctet rid-extractProbeKeyIdentifier (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid _ _)))- | BL.null rid = KeyIdentifierWildcard- | otherwise = KeyIdentifierFingerprint (Fingerprint rid)-extractProbeKeyIdentifier _ = KeyIdentifierWildcard--isWildcardV3RecipientKeyId :: EightOctetKeyId -> Bool-isWildcardV3RecipientKeyId (EightOctetKeyId rid) =- BL.length rid == 8 && BL.all (== 0) rid---- | Extract the public-key algorithm from a PKESK probe packet.-extractProbePKA :: Pkt -> PubKeyAlgorithm-extractProbePKA (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ _ pka _))) = pka-extractProbePKA (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 _ pka _))) = pka-extractProbePKA _ = RSA -- fallback; should not be reached---- | Core implementation: manual await loop so we can return a 'DecryptOutcome'.-conduitDecryptChecked' ::- (MonadFail m, MonadUnliftIO m, MonadResource m, MonadThrow m)- => RecursorState- -> Bool- -> PKESKResolver IO- -> InputCallback IO- -> Maybe (IORef [DecryptSessionKeyResolutionReport])- -> ConduitT Pkt Pkt m DecryptOutcome-conduitDecryptChecked' rs0 allowManualPKESKPrompt pkcb cb reportRef =- loop (SomeDecryptStreamState (ActiveDecryptState rs0))- where- loop ::- (MonadFail m, MonadUnliftIO m, MonadResource m, MonadThrow m)- => SomeDecryptStreamState -> ConduitT Pkt Pkt m DecryptOutcome- loop streamState = do- case streamState of- SomeDecryptStreamState (MalformedDecryptState _ reason) ->- return (DecryptMalformedStructure reason)- SomeDecryptStreamState state -> do- mpkt <- await- case mpkt of- Nothing -> return (finalOutcome state)- Just pkt -> do- (state', pkts) <- lift (push pkt state)- mapM_ yield pkts- loop state'-- push ::- (MonadFail m, MonadUnliftIO m, MonadResource m, MonadThrow m)- => Pkt- -> DecryptStreamState phase- -> m (SomeDecryptStreamState, [Pkt])- push i (ActiveDecryptState s)- | _depth s > 42 = fail "I think we've been quine-attacked"- | hasPendingESKPrelude s && not (packetCanFollowESKPrelude i) =- return- ( SomeDecryptStreamState- (MalformedDecryptState- s- "Malformed encrypted packet sequence: ESK packets must immediately precede encrypted data")- , [] )- | otherwise =- let dp = _decryptPolicy s- in case i of- SKESKPkt payload ->- do- when (decryptRejectDeprecatedSKESK dp) $- case skeskPayloadS2K payload of- Simple _ ->- fail "SKESK uses Simple S2K specifier, which is deprecated by RFC9580 policy"- Salted _ _ ->- fail "SKESK uses Salted S2K specifier, which is deprecated by RFC9580 policy"- _ -> pure ()- return- ( SomeDecryptStreamState- (ActiveDecryptState- (s {_pendingESKs = _pendingESKs s ++ [PendingSKESK payload]}))- , [] )- PKESKPkt p ->- return- ( SomeDecryptStreamState- (ActiveDecryptState- (s {_pendingESKs = _pendingESKs s ++ [PendingPKESK p]}))- , [] )- (SymEncDataPkt bs) ->- if hasESKPayloadVersionMismatch dp LegacyEncryptedPayload s- then- return- ( SomeDecryptStreamState- (MalformedDecryptState- s- ("ESK/payload version mismatch: ESK packets present but none are version-aligned with " ++- "legacy SED payload"))- , [] )- else do- when (not (decryptAllowSEDNoIntegrity dp)) $- fail- "Received unauthenticated SED (Symmetrically Encrypted Data) packet; \- \RFC9580 policy requires integrity-protected SEIPD. \- \Use lenientDecryptPolicy to permit legacy messages."- (symalgo, sessionKey) <-- resolveSessionKey- s- allowManualPKESKPrompt- pkcb- cb- LegacyEncryptedPayload- reportRef- checkDecryptSymmetricAlgo dp symalgo- d <-- decryptSEDP- s {_pendingESKs = []}- allowManualPKESKPrompt- pkcb- cb- reportRef- symalgo- sessionKey- bs- -- SED is the terminal outer-stream packet.- return (finalizeOuterEncryptedPayload s d)- (SymEncIntegrityProtectedDataPkt (SEIPD1 _ bs)) ->- if hasESKPayloadVersionMismatch dp LegacyEncryptedPayload s- then- return- ( SomeDecryptStreamState- (MalformedDecryptState- s- ("ESK/payload version mismatch: ESK packets present but none are version-aligned with " ++- "SEIPDv1 payload"))- , [] )- else do- when (not (decryptAllowSEIPDv1 dp)) $- fail- "Received SEIPDv1 packet; decrypt policy requires SEIPDv2 only."- (symalgo, sessionKey) <-- resolveSessionKey- s- allowManualPKESKPrompt- pkcb- cb- LegacyEncryptedPayload- reportRef- checkDecryptSymmetricAlgo dp symalgo- d <-- decryptSEIPDP- s {_pendingESKs = []}- allowManualPKESKPrompt- pkcb- cb- reportRef- symalgo- sessionKey- bs- -- The outer SEIPD1 packet is terminal; inner MDC is handled by- -- the recursive conduit's own _seenMessageEnd tracking.- return (finalizeOuterEncryptedPayload s d)- (SymEncIntegrityProtectedDataPkt (SEIPD2 sa aa chunkSize salt bs)) ->- if hasESKPayloadVersionMismatch dp (SEIPDv2EncryptedPayload sa aa) s- then- return- ( SomeDecryptStreamState- (MalformedDecryptState- s- ("ESK/payload version mismatch: ESK packets present but none are version-aligned with " ++- "SEIPDv2 payload"))- , [] )- else do- checkDecryptSymmetricAlgo dp sa- checkDecryptAEADAlgo dp aa- (_, sessionKey) <-- resolveSessionKey- s- allowManualPKESKPrompt- pkcb- cb- (SEIPDv2EncryptedPayload sa aa)- reportRef- d <-- decryptSEIPDv2P- s {_pendingESKs = []}- allowManualPKESKPrompt- pkcb- cb- reportRef- sa- aa- chunkSize- salt- sessionKey- bs- -- SEIPD2 final AEAD tag was verified inside decryptSEIPDv2P.- return (finalizeOuterEncryptedPayload s d)- m@(ModificationDetectionCodePkt mdc) -> do- when (isNothing (_lastClearText s)) $ fail "MDC with no referent"- let mcalculated = calculateMDC <$> _lastNonce s <*> _lastClearText s- expectedMdc <-- case mcalculated of- Nothing -> fail "MDC with no nonce or cleartext"- Just Nothing -> fail "MDC referent is too short"- Just (Just x) -> return x- when (expectedMdc /= mdc) $- fail $- "MDC indicates tampering: " ++- show mdc ++- " versus " ++- maybe "<empty>" show mcalculated ++- " ... " ++- show (_lastNonce s) ++ " / " ++ show (_lastClearText s)- -- MDC is the integrity boundary inside a SEIPD1 inner stream.- return- ( SomeDecryptStreamState (FinishedDecryptState s False)- , [m] )- -- RFC9580 Padding Packet (tag 21) can be ignored after decryption.- (OtherPacketPkt 21 _) ->- return (SomeDecryptStreamState (ActiveDecryptState s), [])- (OtherPacketPkt t _) | t < 40 ->- fail ("Unknown critical packet type in packet sequence: " ++ show t)- (OtherPacketPkt _ _) ->- return (SomeDecryptStreamState (ActiveDecryptState s), [])- p ->- return (SomeDecryptStreamState (ActiveDecryptState s), [p])- push i (FinishedDecryptState s hadTrailing) =- if decryptRejectTrailingData (_decryptPolicy s)- then- return- ( SomeDecryptStreamState- (MalformedDecryptState- s- "packet received after message integrity boundary")- , [] )- else- return- (SomeDecryptStreamState (FinishedDecryptState s True), [i])- push _ malformedState@(MalformedDecryptState _ _) =- return (SomeDecryptStreamState malformedState, [])-- hasPendingESKPrelude s = not (null (_pendingESKs s))-- hasESKPayloadVersionMismatch dp payloadFlavor state =- decryptRejectESKVersionMismatch dp- && hasPendingESKPrelude state- && null (alignedPrecedingESKs payloadFlavor (_pendingESKs state))-- finalizeOuterEncryptedPayload state decryptedPkts =- ( SomeDecryptStreamState- (FinishedDecryptState (state { _pendingESKs = [] }) False)- , decryptedPkts )-- finalOutcome :: DecryptStreamState phase -> DecryptOutcome- finalOutcome (ActiveDecryptState _) = DecryptTruncated- finalOutcome (FinishedDecryptState _ hadTrailing) =- if hadTrailing- then DecryptTrailingData- else DecryptClean- finalOutcome (MalformedDecryptState _ reason) =- DecryptMalformedStructure reason-- packetCanFollowESKPrelude pkt =- case pkt of- SKESKPkt _ -> True- PKESKPkt _ -> True- SymEncDataPkt _ -> True- SymEncIntegrityProtectedDataPkt _ -> True- MarkerPkt _ -> True- OtherPacketPkt 21 _ -> True- _ -> False---- | Describes what integrity-terminating marker (if any) an inner packet--- stream is expected to contain. Passed to 'checkInnerOutcome' to--- distinguish a legitimate end-of-stream from a missing marker.-data InnerIntegrityExpectation- = NoIntegrityMarker- -- ^ The inner stream has no integrity-terminating packet. SED has none- -- by design; SEIPD2 authenticates via AEAD before the conduit runs.- -- 'DecryptTruncated' is the normal end-of-stream outcome.---- | Propagate non-clean inner-stream outcomes as a 'fail'. Called after each--- recursive decrypt helper so that structural violations and (for SEIPD1) a--- missing MDC are not silently swallowed.-checkInnerOutcome :: MonadFail m => InnerIntegrityExpectation -> DecryptOutcome -> m ()-checkInnerOutcome _ DecryptClean = pure ()-checkInnerOutcome _ DecryptTrailingData = pure ()-checkInnerOutcome NoIntegrityMarker DecryptTruncated = pure ()-checkInnerOutcome _ (DecryptMalformedStructure reason) =- fail ("Inner encrypted payload had malformed structure: " ++ reason)--decryptSEDP ::- (MonadFail m, MonadUnliftIO m, MonadIO m, MonadThrow m)- => RecursorState- -> Bool- -> PKESKResolver IO- -> InputCallback IO- -> Maybe (IORef [DecryptSessionKeyResolutionReport])- -> SymmetricAlgorithm- -> SessionKey- -> BL.ByteString- -> m [Pkt]-decryptSEDP rs allowManualPKESKPrompt pkcb cb reportRef symalgo (SessionKey sessionKey) bs = do- decrypted <-- case decryptOpenPGPCfb symalgo (BL.toStrict bs) sessionKey of- Left e -> fail (renderCipherError e)- Right x -> pure x- (innerOutcome, pkts) <- decryptInnerPackets rs allowManualPKESKPrompt pkcb cb reportRef decrypted- checkInnerOutcome NoIntegrityMarker innerOutcome- pure pkts--decryptSEIPDP ::- (MonadFail m, MonadUnliftIO m, MonadIO m, MonadThrow m)- => RecursorState- -> Bool- -> PKESKResolver IO- -> InputCallback IO- -> Maybe (IORef [DecryptSessionKeyResolutionReport])- -> SymmetricAlgorithm- -> SessionKey- -> BL.ByteString- -> m [Pkt]-decryptSEIPDP rs allowManualPKESKPrompt pkcb cb reportRef symalgo (SessionKey sessionKey) bs = do- (nonce, decrypted) <-- case decryptPreservingNonce symalgo (BL.toStrict bs) sessionKey of- Left e -> fail (renderCipherError e)- Right x -> pure x- decryptedWithoutMDC <-- case validateSEIPD1MDC nonce decrypted of- Left err -> fail err- Right x -> pure x- (innerOutcome, pkts) <- decryptInnerPackets rs allowManualPKESKPrompt pkcb cb reportRef decryptedWithoutMDC- checkInnerOutcome NoIntegrityMarker innerOutcome- pure pkts--decryptSEIPDv2P ::- (MonadFail m, MonadUnliftIO m, MonadIO m, MonadThrow m)- => RecursorState- -> Bool- -> PKESKResolver IO- -> InputCallback IO- -> Maybe (IORef [DecryptSessionKeyResolutionReport])- -> SymmetricAlgorithm- -> AEADAlgorithm- -> Word8- -> Salt- -> SessionKey- -> BL.ByteString- -> m [Pkt]-decryptSEIPDv2P rs allowManualPKESKPrompt pkcb cb reportRef symalgo aeadalgo chunkSize salt sessionKey bs = do- let decrypted =- decryptSEIPDv2Payload- symalgo- aeadalgo- chunkSize- salt- (BL.toStrict bs)- sessionKey- case decrypted of- Left e -> fail e- Right cleartext -> do- (innerOutcome, pkts) <-- decryptInnerPackets rs allowManualPKESKPrompt pkcb cb reportRef cleartext- checkInnerOutcome NoIntegrityMarker innerOutcome- pure pkts--decryptInnerPackets ::- (MonadFail m, MonadUnliftIO m, MonadThrow m)- => RecursorState- -> Bool- -> PKESKResolver IO- -> InputCallback IO- -> Maybe (IORef [DecryptSessionKeyResolutionReport])- -> B.ByteString- -> m (DecryptOutcome, [Pkt])-decryptInnerPackets rs allowManualPKESKPrompt pkcb cb reportRef cleartext =- runConduitRes $- CB.sourceLbs (BL.fromStrict cleartext) .| conduitGet get .| conduitDecompress .|- fuseBoth- (conduitDecryptChecked'- rs {_depth = _depth rs + 1}- allowManualPKESKPrompt- pkcb- cb- reportRef)- CL.consume--decryptSEIPDv2Payload ::- SymmetricAlgorithm- -> AEADAlgorithm- -> Word8- -> Salt- -> B.ByteString- -> SessionKey- -> Either String B.ByteString-decryptSEIPDv2Payload symalgo aeadalgo chunkSize salt encrypted (SessionKey sessionKey) = do- when (chunkSize > 16) $- Left "SEIPD v2 chunk size octet must be between 0 and 16"- (mode, nonceSize) <- aeadModeAndNonceSize aeadalgo- keyLen <- symKeySize symalgo- let outputLen = keyLen + nonceSize - 8- when (B.length (unSalt salt) /= 32) $- Left "SEIPD v2 salt must be exactly 32 octets"- when (B.length encrypted < 32) $- Left "SEIPD v2 ciphertext must include at least one chunk tag and a final tag"- let info =- B.pack- [0xd2, 2, fromFVal symalgo, fromFVal aeadalgo, chunkSize]- prk = extract @CHA.SHA256 (unSalt salt) sessionKey- okm = expand @CHA.SHA256 prk info outputLen :: B.ByteString- messageKey = B.take keyLen okm- noncePrefix = B.take (nonceSize - 8) (B.drop keyLen okm)- decryptSEIPDv2WithKey- symalgo- mode- chunkSize- info- noncePrefix- messageKey- encrypted--decryptSEIPDv2WithKey ::- SymmetricAlgorithm- -> CCT.AEADMode- -> Word8- -> B.ByteString- -> B.ByteString- -> B.ByteString- -> B.ByteString- -> Either String B.ByteString-decryptSEIPDv2WithKey symalgo mode chunkSize info noncePrefix sessionKey encrypted =- withAESCipher- "SEIPD v2 decrypt currently supports AES-128/192/256 only"- symalgo- sessionKey- (decryptChunks mode info chunkSize noncePrefix encrypted)--decryptChunks ::- CCT.BlockCipher cipher- => CCT.AEADMode- -> B.ByteString- -> Word8- -> B.ByteString- -> B.ByteString- -> cipher- -> Either String B.ByteString-decryptChunks 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- decryptChunksWithReader = go 0 encrypted [] 0- where- chunkLen = 1 `shiftL` (fromIntegral chunkSize + 6)- tagLen = 16- - go idx remaining acc totalPlain- | B.length remaining < 2 * tagLen =- lift $ Left "SEIPD v2 ciphertext is too short for chunk and final authentication tags"- | otherwise = do- let hasMoreChunks = B.length remaining > chunkLen + 2 * tagLen- currentChunkLen =- if hasMoreChunks- then chunkLen- else B.length remaining - 2 * tagLen- when (currentChunkLen < 0) $- lift $ Left "SEIPD v2 malformed chunk lengths"- let (chunkCiphertext, r1) = B.splitAt currentChunkLen remaining- (chunkTag, r2) = B.splitAt tagLen r1- plainChunk <- decryptChunkWithContext idx chunkCiphertext chunkTag- if hasMoreChunks- then go- (idx + 1)- r2- (plainChunk : acc)- (totalPlain + B.length plainChunk)- else do- when (B.length r2 /= tagLen) $- lift $ Left "SEIPD v2 missing final authentication tag"- verifyFinalTagWithContext- (idx + 1)- (totalPlain + B.length plainChunk)- r2- return (B.concat (reverse (plainChunk : acc)))- - decryptChunkWithContext idx chunkCiphertext chunkTag = do- AEADDecryptContext mode' _ _ noncePrefix' cipher' <- ask- if mode' == CCT.AEAD_OCB- then- lift $- decryptWithOCBRFC7253With- (\_ _ _ _ _ _ -> "SEIPD v2 chunk authentication failed")- cipher'- (noncePrefix' <> encodeWord64be idx)- info- chunkCiphertext- (mkAuthTag chunkTag)- else do- aead <- initAEADWithContext idx- let mPlain =- CCT.aeadSimpleDecrypt- aead- info- chunkCiphertext- (mkAuthTag chunkTag)- case mPlain of- Nothing -> lift $ Left "SEIPD v2 chunk authentication failed"- Just p -> return p- - verifyFinalTagWithContext idx totalPlain finalTag = do- AEADDecryptContext mode' _ _ noncePrefix' cipher' <- ask- if mode' == CCT.AEAD_OCB- then do- plain <-- lift $- decryptWithOCBRFC7253With- (\_ _ _ _ _ _ -> "SEIPD v2 chunk authentication failed")- cipher'- (noncePrefix' <> encodeWord64be idx)- (info <> encodeWord64be (fromIntegral totalPlain))- B.empty- (mkAuthTag finalTag)- if B.null plain- then return ()- else lift $ Left "SEIPD v2 final authentication tag verification failed"- else do- aead <- initAEADWithContext idx- let mEmpty =- CCT.aeadSimpleDecrypt- aead- (info <> encodeWord64be (fromIntegral totalPlain))- B.empty- (mkAuthTag finalTag)- case mEmpty of- Just p | B.null p -> return ()- _ -> lift $ Left "SEIPD v2 final authentication tag verification failed"- - initAEADWithContext idx = do- AEADDecryptContext mode' _ _ noncePrefix' cipher' <- ask- lift $ first show . CE.eitherCryptoError $- CCT.aeadInit mode' cipher' (noncePrefix' <> encodeWord64be idx)--aeadModeAndNonceSize :: AEADAlgorithm -> Either String (CCT.AEADMode, Int)-aeadModeAndNonceSize =- aeadModeAndNonceSizeForSEIPDv2- "Unknown AEAD algorithm for SEIPD v2 decrypt"--symKeySize :: SymmetricAlgorithm -> Either String Int-symKeySize =- seipdv2SymmetricKeySize- "SEIPD v2 decrypt currently supports AES-128/192/256 only"--encodeWord64be :: Word64 -> B.ByteString-encodeWord64be = BL.toStrict . runPut . putWord64be--mkAuthTag :: B.ByteString -> CCT.AuthTag-mkAuthTag = CCT.AuthTag . BA.convert--checkDecryptSymmetricAlgo :: MonadFail m => DecryptPolicy -> SymmetricAlgorithm -> m ()-checkDecryptSymmetricAlgo dp sa =- case decryptAllowedSymmetricAlgos dp of- Nothing -> pure ()- Just allowed- | sa `elem` allowed -> pure ()- | otherwise ->- fail $- "Decrypt policy rejects symmetric algorithm " ++- show sa ++- "; allowed: " ++- show allowed--checkDecryptAEADAlgo :: MonadFail m => DecryptPolicy -> AEADAlgorithm -> m ()-checkDecryptAEADAlgo dp aa =- case decryptAllowedAEADAlgos dp of- Nothing -> pure ()- Just allowed- | aa `elem` allowed -> pure ()- | otherwise ->- fail $- "Decrypt policy rejects AEAD algorithm " ++- show aa ++- "; allowed: " ++- show allowed--skeskPayloadSymmetricAlgorithm :: SKESKPayload -> SymmetricAlgorithm-skeskPayloadSymmetricAlgorithm payload =- case classifySKESKPayload payload of- ClassifiedSKESKPayloadV4 (SKESKPayloadV4 sa _ _) -> sa- ClassifiedSKESKPayloadV6 (SKESKPayloadV6 sa _ _ _ _ _) -> sa--skeskPayloadS2K :: SKESKPayload -> S2K-skeskPayloadS2K payload =- case classifySKESKPayload payload of- ClassifiedSKESKPayloadV4 (SKESKPayloadV4 _ s2k _) -> s2k- ClassifiedSKESKPayloadV6 (SKESKPayloadV6 _ _ s2k _ _ _) -> s2k--skeskPayloadAEADAlgorithm :: SKESKPayload -> Maybe AEADAlgorithm-skeskPayloadAEADAlgorithm payload =- case classifySKESKPayload payload of- ClassifiedSKESKPayloadV4 _ -> Nothing- ClassifiedSKESKPayloadV6 (SKESKPayloadV6 _ aa _ _ _ _) -> Just aa--resolveSKESKSessionKey :: BL.ByteString -> SKESKPayload -> Either String B.ByteString-resolveSKESKSessionKey passphrase payload =- first renderSKESKSessionKeyResolutionError $- resolveSKESKSessionKeyTyped passphrase (classifySKESKPayload payload)--data SKESKSessionKeyResolutionError- = SKESKSessionKeyS2KError S2KError- | SKESKSessionKeyOtherError String- deriving (Eq, Show)--renderSKESKSessionKeyResolutionError :: SKESKSessionKeyResolutionError -> String-renderSKESKSessionKeyResolutionError (SKESKSessionKeyS2KError err) = renderS2KError err-renderSKESKSessionKeyResolutionError (SKESKSessionKeyOtherError err) = err--resolveSKESKSessionKeyTyped ::- BL.ByteString- -> ClassifiedSKESKPayload- -> Either SKESKSessionKeyResolutionError B.ByteString-resolveSKESKSessionKeyTyped passphrase (ClassifiedSKESKPayloadV4 (SKESKPayloadV4 sa s2k Nothing)) =- first- SKESKSessionKeyS2KError- (skesk2Key (SKESK4Packet sa s2k Nothing) passphrase)-resolveSKESKSessionKeyTyped passphrase (ClassifiedSKESKPayloadV4 (SKESKPayloadV4 sa s2k (Just esk))) =- first- SKESKSessionKeyS2KError- (snd <$> skesk2SessionKey (SKESK4Packet sa s2k (Just esk)) passphrase)-resolveSKESKSessionKeyTyped passphrase (ClassifiedSKESKPayloadV6 (SKESKPayloadV6 sa aead s2k iv esk tag)) = do- keyLen <- first (SKESKSessionKeyS2KError . S2KUnsupportedAlgorithm) (keySize sa)- ikm <- first SKESKSessionKeyS2KError (string2Key s2k keyLen passphrase)- kek <- first SKESKSessionKeyOtherError (deriveSKESK6KEK sa aead ikm)- first- SKESKSessionKeyOtherError- (decryptSKESK6SessionKey sa aead kek (BL.toStrict iv) (BL.toStrict esk) (BL.toStrict tag))--data ClassifiedSKESKPayload where- ClassifiedSKESKPayloadV4 :: SKESKPayloadV4 -> ClassifiedSKESKPayload- ClassifiedSKESKPayloadV6 :: SKESKPayloadV6 -> ClassifiedSKESKPayload--classifySKESKPayload :: SKESKPayload -> ClassifiedSKESKPayload-classifySKESKPayload (SKESKPayloadV4Packet payloadV4) =- ClassifiedSKESKPayloadV4 payloadV4-classifySKESKPayload (SKESKPayloadV6Packet payloadV6) =- ClassifiedSKESKPayloadV6 payloadV6--resolveSessionKey ::- (MonadFail m, MonadIO m)- => RecursorState- -> Bool- -> PKESKResolver IO- -> InputCallback IO- -> EncryptedPayloadFlavor v- -> Maybe (IORef [DecryptSessionKeyResolutionReport])- -> m (SymmetricAlgorithm, SessionKey)-resolveSessionKey rs allowManualPKESKPrompt pkcb cb payloadFlavor reportRef =- case precedingCandidates of- [] ->- if null (_pendingESKs rs)- then fail "Encrypted data packet has no preceding SKESK or PKESK packet"- else- fail- "Encrypted data packet has no preceding SKESK or PKESK packet aligned with payload version"- candidates ->- case skeskCandidates candidates of- [] -> resolvePKESKCandidates (pkeskCandidates candidates) [] [] []- skesks -> do- passphrase <- liftIO $ cb "Input the passphrase I want"- resolveSKESKCandidates passphrase skesks (pkeskCandidates candidates) []- where- precedingCandidates- | strictAlignment = alignedByVersion- | null alignedByVersion = _pendingESKs rs- | otherwise = alignedByVersion-- strictAlignment = decryptRejectESKVersionMismatch (_decryptPolicy rs)- alignedByVersion = pendingESKsFromAligned alignedStrict- alignedStrict = alignedPrecedingESKs payloadFlavor (_pendingESKs rs)-- skeskCandidates esks = reverse [skesk | PendingSKESK skesk <- esks]-- pkeskCandidates esks = reverse [pkesk | PendingPKESK pkesk <- esks]-- resolveSKESKCandidates ::- (MonadFail m, MonadIO m)- => BL.ByteString- -> [SKESKPayload]- -> [PKESKPayload]- -> [String]- -> m (SymmetricAlgorithm, SessionKey)- resolveSKESKCandidates _ [] pkesks skeskErrs =- resolvePKESKCandidates pkesks skeskErrs [] []- resolveSKESKCandidates passphrase (skesk:rest) pkesks skeskErrs =- case resolveSKESKCandidate passphrase skesk of- Left err ->- resolveSKESKCandidates- passphrase- rest- pkesks- ((skeskErrPrefix skesk ++ err) : skeskErrs)- Right resolved -> do- emitResolutionReport- (mkResolutionReport DecryptResolvedViaSKESK skeskErrs [] [])- pure resolved-- resolveSKESKCandidate ::- BL.ByteString- -> SKESKPayload- -> Either String (SymmetricAlgorithm, SessionKey)- resolveSKESKCandidate passphrase skesk = do- let skeskSymAlgo = skeskPayloadSymmetricAlgorithm skesk- expectedSymAlgo = payloadExpectedSymmetricAlgorithm payloadFlavor- sessionKeyBytes <- resolveSKESKSessionKey passphrase skesk- case expectedSymAlgo of- Just expected | expected /= skeskSymAlgo ->- Left "SKESK/encrypted-payload symmetric algorithm mismatch"- _ ->- case (payloadExpectedAEADAlgorithm payloadFlavor, skeskPayloadAEADAlgorithm skesk) of- (Just expectedAEAD, Just skeskAEAD)- | expectedAEAD /= skeskAEAD ->- Left "SKESK/encrypted-payload AEAD algorithm mismatch"- _ -> Right (skeskSymAlgo, SessionKey sessionKeyBytes)-- skeskErrPrefix skesk = "[" ++ describeSKESK skesk ++ "] "-- resolvePKESKCandidates ::- (MonadFail m, MonadIO m)- => [PKESKPayload]- -> [String]- -> [String]- -> [PKESKResolverAttempt]- -> m (SymmetricAlgorithm, SessionKey)- resolvePKESKCandidates [] [] [] _ =- fail "Encrypted data packet has no usable preceding SKESK or PKESK packet"- resolvePKESKCandidates [] skeskErrs [] _ =- fail- ("Encrypted data packet has no usable preceding SKESK or PKESK packet; " ++- "candidate errors: " ++ unwords (reverse skeskErrs))- resolvePKESKCandidates [] skeskErrs pkeskErrs resolverAttempts =- if allowManualPKESKPrompt- then do- let expectedSymAlgo = payloadExpectedSymmetricAlgorithm payloadFlavor- errs = skeskErrs ++ pkeskErrs- encodedSessionKey <-- BL.toStrict <$>- liftIO- (cb- "Input decrypted PKESK session key material (OpenPGP encoded or raw key bytes)")- case decodePKESKSessionKey expectedSymAlgo encodedSessionKey of- Left manualErr ->- fail- ("Encrypted data packet has no usable preceding SKESK or PKESK packet; " ++- "candidate errors: " ++- unwords (reverse errs) ++- "; manual input failed: " ++ manualErr)- Right (sa, k) -> do- emitResolutionReport- (mkResolutionReport- DecryptResolvedViaManualPKESKInput- skeskErrs- pkeskErrs- resolverAttempts)- pure (sa, SessionKey k)- else- fail- ("Encrypted data packet has no usable preceding SKESK or PKESK packet; " ++- "candidate errors: " ++- unwords (reverse (skeskErrs ++ pkeskErrs)))- resolvePKESKCandidates (pkesk:rest) skeskErrs pkeskErrs resolverAttempts = do- let expectedSymAlgo = payloadExpectedSymmetricAlgorithm payloadFlavor- errPrefix = "[" ++ describePKESK pkesk ++ "] "- attemptPKESKCandidate [] [] 0 expectedSymAlgo errPrefix resolverAttempts- where- attemptPKESKCandidate attemptedSKeys previousFailures attemptIndex expectedSymAlgo errPrefix resolverAttemptsAcc = do- let callbackProbeSummary = describeCallbackProbeSummary pkesk- resolverResult <-- liftIO (resolvePKESKRecipientKey pkesk previousFailures attemptIndex)- case resolverResult of- Left resolverErr ->- fail (errPrefix ++ resolverErr)- Right (Nothing, _, newAttempts) ->- let resolverAttempts' = resolverAttemptsAcc ++ newAttempts- terminalError =- case reverse previousFailures of- (latestFailure:_) -> errPrefix ++ pkeskAttemptFailureReason latestFailure- [] ->- errPrefix ++- "no matching key context (callback probes: " ++- callbackProbeSummary ++- ")"- in- resolvePKESKCandidates- rest- skeskErrs- (terminalError : pkeskErrs)- resolverAttempts'- Right (Just keyInfo, nextAttemptIndex, newAttempts)- | pkeskRecipientSKey keyInfo `elem` attemptedSKeys ->- let resolverAttempts' = resolverAttemptsAcc ++ newAttempts- terminalError =- case reverse previousFailures of- (latestFailure:_) -> errPrefix ++ pkeskAttemptFailureReason latestFailure- [] ->- errPrefix ++- "key context callback repeated without yielding a usable key"- in- resolvePKESKCandidates- rest- skeskErrs- (terminalError : pkeskErrs)- resolverAttempts'- | otherwise -> do- let resolverAttempts' = resolverAttemptsAcc ++ newAttempts- unwrapped <- liftIO (tryUnwrapPKESKSessionMaterial pkesk keyInfo)- case unwrapped of- Left err ->- attemptPKESKCandidate- (pkeskRecipientSKey keyInfo : attemptedSKeys)- (previousFailures ++ [mkAttemptFailure keyInfo PKESKAttemptUnwrapFailed err])- nextAttemptIndex- expectedSymAlgo- errPrefix- resolverAttempts'- Right encodedSessionKey ->- case decodePKESKSessionKey expectedSymAlgo encodedSessionKey of- Left err ->- attemptPKESKCandidate- (pkeskRecipientSKey keyInfo : attemptedSKeys)- (previousFailures ++- [ mkAttemptFailure- keyInfo- PKESKAttemptSessionMaterialDecodeFailed- err- ])- nextAttemptIndex- expectedSymAlgo- errPrefix- resolverAttempts'- Right (sa, k) -> do- emitResolutionReport- (mkResolutionReport- DecryptResolvedViaPKESK- skeskErrs- pkeskErrs- resolverAttempts')- pure (sa, SessionKey k)-- emitResolutionReport :: MonadIO m => DecryptSessionKeyResolutionReport -> m ()- emitResolutionReport report =- case reportRef of- Nothing -> pure ()- Just ref -> liftIO (modifyIORef' ref (report :))-- mkResolutionReport ::- DecryptSessionKeyResolutionPath- -> [String]- -> [String]- -> [PKESKResolverAttempt]- -> DecryptSessionKeyResolutionReport- mkResolutionReport path skeskErrs pkeskErrs resolverAttempts =- DecryptSessionKeyResolutionReport- { decryptSessionResolutionPath = path- , decryptSessionResolutionSKESKErrors = reverse skeskErrs- , decryptSessionResolutionPKESKErrors = reverse pkeskErrs- , decryptSessionResolutionResolverAttempts = resolverAttempts- }-- mkAttemptFailure keyInfo failureKind reason =- PKESKAttemptFailure- { pkeskAttemptFailureKeyContext =- recipientKeyContext keyInfo- , pkeskAttemptFailureKind = failureKind- , pkeskAttemptFailureReason = reason- }-- recipientKeyContext :: PKESKRecipientKey -> Maybe (KeyVersion, PubKeyAlgorithm)- recipientKeyContext keyInfo =- fmap (\pk -> (_keyVersion pk, _pkalgo pk)) (pkeskRecipientPKPayload keyInfo)-- renderPKESKResolverError (ResolverPolicyDenied reason) =- "resolver policy denied candidate selection: " ++ reason- renderPKESKResolverError (ResolverBackendUnavailable reason) =- "resolver backend unavailable: " ++ reason- renderPKESKResolverError (ResolverInvalidResponse reason) =- "resolver returned an invalid response: " ++ reason-- resolvePKESKRecipientKey payload previousFailures attemptIndex0 =- probePacketVariants attemptIndex0 [] (pkeskCallbackPackets payload)- where- probePacketVariants attemptIndex attemptsAcc [] =- pure (Right (Nothing, attemptIndex, reverse attemptsAcc))- probePacketVariants attemptIndex attemptsAcc (probePkt:restProbePkts) = do- let request =- PKESKResolveRequest- { reqPKESK = payload- , reqProbePacket = probePkt- , reqIsWildcardRecipient = isWildcardPKESKPayload payload- , reqAttemptIndex = attemptIndex- , reqPreviousFailures = previousFailures- }- resolveAction <-- pkcb request- let attemptRecord =- PKESKResolverAttempt- { pkeskResolverAttemptPreviousFailures = previousFailures- , pkeskResolverAttemptAction =- case resolveAction of- ResolveWith keyInfo ->- ResolverAttemptResolveWith (recipientKeyContext keyInfo)- ResolveSkip -> ResolverAttemptSkip- ResolveExhausted -> ResolverAttemptExhausted- ResolveFail resolverErr -> ResolverAttemptFail resolverErr- }- case resolveAction of- ResolveWith keyInfo ->- pure- (Right (Just keyInfo, attemptIndex + 1, reverse (attemptRecord : attemptsAcc)))- ResolveSkip ->- probePacketVariants- (attemptIndex + 1)- (attemptRecord : attemptsAcc)- restProbePkts- ResolveExhausted ->- pure- (Right (Nothing, attemptIndex + 1, reverse (attemptRecord : attemptsAcc)))- ResolveFail resolverErr ->- pure (Left (renderPKESKResolverError resolverErr))-- pkeskCallbackPackets payload =- nub $- case payload of- PKESKPayloadV3Packet (PKESKPayloadV3 v rid pka mpis) ->- map- (\ridVariant ->- PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 v ridVariant pka mpis)))- (recipientIdCallbackVariantsV3 rid)- PKESKPayloadV6Packet (PKESKPayloadV6 rid pka esk) ->- map- (\ridVariant ->- PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 ridVariant pka esk)))- (recipientIdCallbackVariants rid)-- describeCallbackProbeSummary payload =- intercalate ", " (map describePKESKCallbackProbe (pkeskCallbackPackets payload))-- describePKESKCallbackProbe (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ rid pka _))) =- "PKESK3 " ++- show pka ++- " rid=" ++- show rid ++- if isWildcardV3RecipientKeyId rid- then " (wildcard)"- else ""- describePKESKCallbackProbe (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka _))) =- "PKESK6 " ++ show pka ++ " rid=" ++ show rid- describePKESKCallbackProbe pkt = show pkt-- recipientIdCallbackVariantsV3 rid- | isWildcardV3RecipientKeyId rid = [rid]- | otherwise = [rid, EightOctetKeyId (BL.replicate 8 0)]-- isWildcardV3RecipientKeyId (EightOctetKeyId rid) =- BL.length rid == 8 && BL.all (== 0) rid-- recipientIdCallbackVariants rid- | BL.length rid == 20 = [rid, BL.cons 0x04 rid]- | BL.length rid == 21 && BL.head rid == 0x04 = [rid, BL.tail rid]- | BL.length rid == 32 = [rid, BL.cons 0x06 rid]- | BL.length rid == 33 && BL.head rid == 0x06 = [rid, BL.tail rid]- | otherwise = [rid]-- isWildcardPKESKPayload (PKESKPayloadV3Packet (PKESKPayloadV3 _ (EightOctetKeyId rid) _ _)) =- isWildcardV3RecipientKeyId (EightOctetKeyId rid)- isWildcardPKESKPayload _ = False- describePKESK payload =- case classifyPKESKPayload payload of- ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ rid pka _) ->- "PKESK3 " ++ show pka ++ " rid=" ++ show rid- ClassifiedPKESKPayloadV6 (PKESKPayloadV6 rid pka _) ->- "PKESK6 " ++ show pka ++ " rid=" ++ show rid-- describeSKESK payload =- case classifySKESKPayload payload of- ClassifiedSKESKPayloadV4 (SKESKPayloadV4 sa s2k _) ->- "SKESK4 " ++ show sa ++ " s2k=" ++ show s2k- ClassifiedSKESKPayloadV6 (SKESKPayloadV6 sa aa s2k _ _ _) ->- "SKESK6 " ++ show sa ++ "/" ++ show aa ++ " s2k=" ++ show s2k--data AlignedPendingESK (v :: EncryptedPayloadVersion) where- LegacyAlignedSKESK :: SKESKPayloadV4 -> AlignedPendingESK 'LegacyEncryptedPayloadVersion- LegacyAlignedPKESK :: PKESKPayloadV3 -> AlignedPendingESK 'LegacyEncryptedPayloadVersion- SEIPDv2AlignedSKESK :: SKESKPayloadV6 -> AlignedPendingESK 'SEIPDv2EncryptedPayloadVersion- SEIPDv2AlignedPKESK :: PKESKPayloadV6 -> AlignedPendingESK 'SEIPDv2EncryptedPayloadVersion- -- | A version 3 PKESK that precedes a SEIPDv2 payload.- --- -- RFC 9580 §5.13 explicitly permits version 3 PKESKs before a version 2- -- SEIPD packet, as a backward-compatibility allowance for implementations- -- that cannot yet produce v6 key material. The v3 PKESK carries the- -- session key wrapped with legacy (v3) asymmetric key wrapping; the SEIPD- -- v2 payload itself is still authenticated with modern AEAD. This- -- constructor therefore counts as "aligned" for SEIPDv2 — it does not- -- indicate a mis-assembled message — even though the PKESK version does- -- not match the SEIPD version.- SEIPDv2AlignedLegacyPKESK :: PKESKPayloadV3 -> AlignedPendingESK 'SEIPDv2EncryptedPayloadVersion--alignedPrecedingESKs ::- EncryptedPayloadFlavor v- -> [PendingESK]- -> [AlignedPendingESK v]-alignedPrecedingESKs LegacyEncryptedPayload =- mapMaybe- (\esk ->- case esk of- PendingSKESK (SKESKPayloadV4Packet skesk4) -> Just (LegacyAlignedSKESK skesk4)- PendingPKESK (PKESKPayloadV3Packet pkesk3) -> Just (LegacyAlignedPKESK pkesk3)- _ -> Nothing)-alignedPrecedingESKs (SEIPDv2EncryptedPayload _ _) =- mapMaybe- (\esk ->- case esk of- PendingSKESK (SKESKPayloadV6Packet skesk6) -> Just (SEIPDv2AlignedSKESK skesk6)- PendingPKESK (PKESKPayloadV6Packet pkesk6) -> Just (SEIPDv2AlignedPKESK pkesk6)- -- v3 PKESK before SEIPDv2 is explicitly allowed by RFC 9580 §5.13;- -- see 'SEIPDv2AlignedLegacyPKESK' for details.- PendingPKESK (PKESKPayloadV3Packet pkesk3) -> Just (SEIPDv2AlignedLegacyPKESK pkesk3)- _ -> Nothing)--pendingESKsFromAligned :: [AlignedPendingESK v] -> [PendingESK]-pendingESKsFromAligned =- map- (\esk ->- case esk of- LegacyAlignedSKESK skesk4 -> PendingSKESK (SKESKPayloadV4Packet skesk4)- LegacyAlignedPKESK pkesk3 -> PendingPKESK (PKESKPayloadV3Packet pkesk3)- SEIPDv2AlignedSKESK skesk6 -> PendingSKESK (SKESKPayloadV6Packet skesk6)- SEIPDv2AlignedPKESK pkesk6 -> PendingPKESK (PKESKPayloadV6Packet pkesk6)- SEIPDv2AlignedLegacyPKESK pkesk3 -> PendingPKESK (PKESKPayloadV3Packet pkesk3))--payloadExpectedSymmetricAlgorithm :: EncryptedPayloadFlavor v -> Maybe SymmetricAlgorithm-payloadExpectedSymmetricAlgorithm LegacyEncryptedPayload = Nothing-payloadExpectedSymmetricAlgorithm (SEIPDv2EncryptedPayload sa _) = Just sa--payloadExpectedAEADAlgorithm :: EncryptedPayloadFlavor v -> Maybe AEADAlgorithm-payloadExpectedAEADAlgorithm LegacyEncryptedPayload = Nothing-payloadExpectedAEADAlgorithm (SEIPDv2EncryptedPayload _ aa) = Just aa--tryUnwrapPKESKSessionMaterial ::- PKESKPayload -> PKESKRecipientKey -> IO (Either String B.ByteString)-tryUnwrapPKESKSessionMaterial pkesk keyInfo = do- attempted <- try @SomeException (unwrapPKESKSessionMaterial pkesk keyInfo :: IO B.ByteString)- pure (first displayException attempted)--data PKESKUnwrapCase where- PKESKUnwrapV3RSA :: RSATypes.PrivateKey -> MPI -> PKESKUnwrapCase- PKESKUnwrapV6RSA :: RSATypes.PrivateKey -> B.ByteString -> PKESKUnwrapCase- PKESKUnwrapV6ECDH ::- PKESKRecipientKey- -> PubKeyAlgorithm- -> B.ByteString- -> ECDSA.PrivateKey- -> PKESKUnwrapCase- PKESKUnwrapV6XDHRaw ::- PKESKRecipientKey- -> PubKeyAlgorithm- -> B.ByteString- -> B.ByteString- -> PKESKUnwrapCase- PKESKUnwrapV3X25519FromECDH ::- PKESKRecipientKey- -> NonEmpty MPI- -> ECDSA.PrivateKey- -> PKESKUnwrapCase- PKESKUnwrapV3X25519Raw ::- PKESKRecipientKey- -> NonEmpty MPI- -> B.ByteString- -> PKESKUnwrapCase- PKESKUnwrapV3ECDH ::- PKESKRecipientKey- -> PubKeyAlgorithm- -> NonEmpty MPI- -> ECDSA.PrivateKey- -> PKESKUnwrapCase--data ClassifiedPKESKPayload where- ClassifiedPKESKPayloadV3 :: PKESKPayloadV3 -> ClassifiedPKESKPayload- ClassifiedPKESKPayloadV6 :: PKESKPayloadV6 -> ClassifiedPKESKPayload--data ClassifiedPKESKRecipientKey where- ClassifiedPKESKRecipientRSA ::- PKESKRecipientKey -> RSATypes.PrivateKey -> ClassifiedPKESKRecipientKey- ClassifiedPKESKRecipientECDH ::- PKESKRecipientKey -> ECDSA.PrivateKey -> ClassifiedPKESKRecipientKey- ClassifiedPKESKRecipientX25519 ::- PKESKRecipientKey -> B.ByteString -> ClassifiedPKESKRecipientKey- ClassifiedPKESKRecipientX448 ::- PKESKRecipientKey -> B.ByteString -> ClassifiedPKESKRecipientKey- ClassifiedPKESKRecipientUnsupported ::- PKESKRecipientKey -> ClassifiedPKESKRecipientKey--classifyPKESKPayload :: PKESKPayload -> ClassifiedPKESKPayload-classifyPKESKPayload (PKESKPayloadV3Packet payloadV3) =- ClassifiedPKESKPayloadV3 payloadV3-classifyPKESKPayload (PKESKPayloadV6Packet payloadV6) =- ClassifiedPKESKPayloadV6 payloadV6--classifyPKESKRecipientKey :: PKESKRecipientKey -> ClassifiedPKESKRecipientKey-classifyPKESKRecipientKey keyInfo@(PKESKRecipientKey _ (RSAPrivateKey (RSA_PrivateKey privateKey))) =- ClassifiedPKESKRecipientRSA keyInfo privateKey-classifyPKESKRecipientKey keyInfo@(PKESKRecipientKey _ (ECDHPrivateKey (ECDSA_PrivateKey privateKey))) =- ClassifiedPKESKRecipientECDH keyInfo privateKey-classifyPKESKRecipientKey keyInfo@(PKESKRecipientKey _ (X25519PrivateKey privateKeyRaw)) =- ClassifiedPKESKRecipientX25519 keyInfo privateKeyRaw-classifyPKESKRecipientKey keyInfo@(PKESKRecipientKey _ (X448PrivateKey privateKeyRaw)) =- ClassifiedPKESKRecipientX448 keyInfo privateKeyRaw-classifyPKESKRecipientKey keyInfo =- ClassifiedPKESKRecipientUnsupported keyInfo--pkeskPayloadAlgorithm :: PKESKPayload -> PubKeyAlgorithm-pkeskPayloadAlgorithm payload =- case classifyPKESKPayload payload of- ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ _ pka _) -> pka- ClassifiedPKESKPayloadV6 (PKESKPayloadV6 _ pka _) -> pka--classifyPKESKUnwrapCase ::- PKESKPayload- -> PKESKRecipientKey- -> Either String PKESKUnwrapCase-classifyPKESKUnwrapCase payload keyInfo =- case (classifyPKESKPayload payload, classifyPKESKRecipientKey keyInfo) of- ( ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ _ pka (mpi :| []))- , ClassifiedPKESKRecipientRSA _ privateKey)- | pka == RSA || pka == DeprecatedRSAEncryptOnly ->- Right (PKESKUnwrapV3RSA privateKey mpi)- ( ClassifiedPKESKPayloadV6 (PKESKPayloadV6 _ pka esk)- , ClassifiedPKESKRecipientRSA _ privateKey)- | pka == RSA ->- Right (PKESKUnwrapV6RSA privateKey (BL.toStrict esk))- ( ClassifiedPKESKPayloadV6 (PKESKPayloadV6 _ pka esk)- , ClassifiedPKESKRecipientECDH recipientCtx privateKey)- | pka == ECDH || pka == X25519 ->- Right (PKESKUnwrapV6ECDH recipientCtx pka (BL.toStrict esk) privateKey)- ( ClassifiedPKESKPayloadV6 (PKESKPayloadV6 _ pka esk)- , ClassifiedPKESKRecipientX25519 recipientCtx privateKeyRaw)- | pka == X25519 ->- Right (PKESKUnwrapV6XDHRaw recipientCtx pka (BL.toStrict esk) privateKeyRaw)- ( ClassifiedPKESKPayloadV6 (PKESKPayloadV6 _ pka esk)- , ClassifiedPKESKRecipientX448 recipientCtx privateKeyRaw)- | pka == X448 ->- Right (PKESKUnwrapV6XDHRaw recipientCtx pka (BL.toStrict esk) privateKeyRaw)- ( ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ _ pka mpis)- , ClassifiedPKESKRecipientECDH recipientCtx privateKey)- | pka == X25519 ->- Right (PKESKUnwrapV3X25519FromECDH recipientCtx mpis privateKey)- ( ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ _ pka mpis)- , ClassifiedPKESKRecipientX25519 recipientCtx privateKeyRaw)- | pka == X25519 ->- Right (PKESKUnwrapV3X25519Raw recipientCtx mpis privateKeyRaw)- ( ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ _ pka mpis)- , ClassifiedPKESKRecipientECDH recipientCtx privateKey)- | pka == ECDH || pka == X25519 ->- Right (PKESKUnwrapV3ECDH recipientCtx pka mpis privateKey)- (ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ _ pka _), _) ->- Left ("PKESK key unwrap unsupported for packet algorithm " ++ show pka)- (ClassifiedPKESKPayloadV6 (PKESKPayloadV6 _ pka _), _) ->- Left- ("PKESKv6 key unwrap unsupported for packet algorithm " ++- show pka ++ " with secret key " ++ show (pkeskRecipientSKey keyInfo))--unwrapPKESKSessionMaterial ::- (MonadFail m, MonadIO m) => PKESKPayload -> PKESKRecipientKey -> m B.ByteString-unwrapPKESKSessionMaterial pkesk keyInfo = do- either fail pure (validateTable30Policy pkesk keyInfo)- unwrapCase <- either fail pure (classifyPKESKUnwrapCase pkesk keyInfo)- case unwrapCase of- PKESKUnwrapV3RSA privateKey mpi ->- rsaUnwrap- privateKey- (leftPadTo (rsaModulusBytes privateKey) (i2osp (unMPI mpi)))- PKESKUnwrapV6RSA privateKey esk -> do- normalized <- either fail pure (normalizePKESKv6RSAEsk privateKey esk)- rsaUnwrap privateKey normalized- PKESKUnwrapV6ECDH recipientCtx pka esk privateKey ->- ecdhUnwrapV6 recipientCtx pka esk privateKey- PKESKUnwrapV6XDHRaw recipientCtx pka esk privateKeyRaw ->- ecdhUnwrapV6XDHRaw recipientCtx pka esk privateKeyRaw- PKESKUnwrapV3X25519FromECDH recipientCtx mpis privateKey -> do- recipientPKP <-- maybe- (fail- "X25519 PKESKv3 unwrap requires recipient PKPayload context; use conduitDecrypt with DecryptWithKeyring or DecryptWithUnwrapCandidatesCallback")- pure- (pkeskRecipientPKPayload recipientCtx)- recipientSecretRaw <-- either fail pure (resolveX25519SecretRaw recipientPKP privateKey)- x25519UnwrapV3 recipientCtx mpis recipientSecretRaw- PKESKUnwrapV3X25519Raw recipientCtx mpis privateKeyRaw ->- x25519UnwrapV3 recipientCtx mpis privateKeyRaw- PKESKUnwrapV3ECDH recipientCtx pka mpis privateKey ->- ecdhUnwrap recipientCtx pka mpis privateKey- where- validateTable30Policy :: PKESKPayload -> PKESKRecipientKey -> Either String ()- validateTable30Policy payload recipientCtx =- case pkeskRecipientPKPayload recipientCtx of- Nothing -> Right ()- Just recipientPKP ->- case (pkeskPayloadAlgorithm payload, _pubkey recipientPKP) of- (ECDH, ECDHPubKey _ kdfHA kdfSA) ->- validateTable30PolicyForRecipient recipientPKP kdfHA kdfSA- _ -> Right ()-- rsaUnwrap privateKey encryptedSessionMaterial = do- decrypted <-- liftIO- (P15.decryptSafer privateKey encryptedSessionMaterial ::- IO (Either RSATypes.Error B.ByteString))- case decrypted of- Left err -> fail ("RSA PKESK decrypt failed: " ++ show err)- Right decoded -> pure decoded-- normalizePKESKv6RSAEsk ::- RSATypes.PrivateKey- -> B.ByteString- -> Either String B.ByteString- normalizePKESKv6RSAEsk privateKey esk = do- when (B.length esk < 2) $- Left "PKESKv6 RSA ESK is too short to contain an MPI"- let mpiBits =- fromIntegral (B.index esk 0) `shiftL` 8 +- fromIntegral (B.index esk 1)- mpiLen = (mpiBits + 7) `div` 8- when (B.length esk /= 2 + mpiLen) $- Left- ("PKESKv6 RSA ESK MPI length mismatch: expected " ++- show (2 + mpiLen) ++ " octets, got " ++ show (B.length esk))- let mpiPayload = B.drop 2 esk- when (mpiBits > 0) $ do- when (B.null mpiPayload) $- Left "PKESKv6 RSA ESK MPI has non-zero bit length but empty payload"- let firstOctet = B.head mpiPayload- actualBits =- (B.length mpiPayload - 1) * 8 + (8 - countLeadingZeros firstOctet)- when (actualBits /= mpiBits) $- Left- ("PKESKv6 RSA ESK MPI bit-length mismatch: declared " ++- show mpiBits ++ ", actual " ++ show actualBits)- let modulusLen = rsaModulusBytes privateKey- when (B.length mpiPayload > modulusLen) $- Left- ("PKESKv6 RSA ESK MPI payload exceeds recipient modulus size: " ++- show (B.length mpiPayload) ++ " > " ++ show modulusLen)- pure (leftPadTo modulusLen mpiPayload)-- ecdhUnwrap recipientCtx pka mpis privateKey = do- recipientPKP <-- maybe- (fail- "ECDH PKESK unwrap requires recipient PKPayload context; use conduitDecrypt with DecryptWithKeyring or DecryptWithUnwrapCandidatesCallback")- pure- (pkeskRecipientPKPayload recipientCtx)- case _pubkey recipientPKP of- ECDHPubKey ecdhPub kdfHA kdfSA -> do- (ephemeralBytes, wrappedSessionKeyBytes) <-- either fail pure (parseECDHPKESKMPIs mpis)- sharedSecret <-- case ecdhPub of- ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _)) -> do- ephPoint <- parseUncompressedPointForCurve curve ephemeralBytes- pure- (BA.convert- (ECCDH.getShared curve (ECDSA.private_d privateKey) ephPoint) ::- B.ByteString)- EdDSAPubKey Ed25519 _ -> do- recipientSecretRaw <-- either fail pure (resolveX25519SecretRaw recipientPKP privateKey)- recipientSecret <-- either fail pure .- first show . CE.eitherCryptoError $- C25519.secretKey recipientSecretRaw- ephBytes <- either fail pure (normalizeX25519EphemeralPublic ephemeralBytes)- ephPub <-- either fail pure .- first show . CE.eitherCryptoError $- C25519.publicKey ephBytes- pure . BA.convert $ C25519.dh ephPub recipientSecret- EdDSAPubKey Ed448 _ ->- fail- "legacy ECDH PKESK unwrap does not support Curve448Legacy recipients"- _ ->- fail- "ECDH PKESK unwrap requires recipient ECDH public key to be ECDSA or X25519-compatible"- param <- either fail pure (buildECDHKDFParam recipientPKP pka ecdhPub kdfHA kdfSA)- kek <- either fail pure (deriveECDHKek kdfHA kdfSA sharedSecret param)- let wrappedCandidates =- candidateWrappedRFC3394CiphertextsForLegacyECDH- (LegacyECDHWrappedRFC3394Ciphertext wrappedSessionKeyBytes)- validUnwraps =- [ material- | wrappedCandidate <- wrappedCandidates- , Right decoded <- [aesKeyUnwrapRFC3394 kdfSA kek (unLegacyECDHWrappedRFC3394Ciphertext wrappedCandidate)]- , Right material <- [parseLegacyECDHDecodedSessionMaterial decoded]- ]- case validUnwraps of- (material:_) -> pure (encodeLegacyECDHSessionMaterial material)- [] ->- case aesKeyUnwrapRFC3394 kdfSA kek wrappedSessionKeyBytes of- Left err -> fail err- Right _ ->- fail- "legacy ECDH wrapped session key decrypted but decoded session material is malformed"- _ ->- fail- "ECDH PKESK unwrap requires recipient PKPayload with ECDHPubKey parameters"-- ecdhUnwrapV6 recipientCtx pka esk privateKey = do- recipientPKP <-- maybe- (fail- "ECDH PKESKv6 unwrap requires recipient PKPayload context; use conduitDecrypt with DecryptWithKeyring or DecryptWithUnwrapCandidatesCallback")- pure- (pkeskRecipientPKPayload recipientCtx)- if pka == X25519- then do- recipientSecretRaw <-- either fail pure (resolveX25519SecretRaw recipientPKP privateKey)- v6X25519Unwrap recipientPKP recipientSecretRaw esk- else- if pka == X448- then- fail- "X448 PKESKv6 unwrap requires an X448PrivateKey recipient secret key and recipient PKPayload context"- else- case _pubkey recipientPKP of- ECDHPubKey ecdhPub kdfHA kdfSA -> do- (ephemeralBytes, wrappedSessionKeyBytes) <-- either fail pure (parsePKESKv6ECDHEsk pka esk)- case ecdhPub of- ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _)) -> do- ephPoint <- parseUncompressedPointForCurve curve ephemeralBytes- let sharedSecret =- BA.convert- (ECCDH.getShared curve (ECDSA.private_d privateKey) ephPoint) ::- B.ByteString- param <- either fail pure (buildECDHKDFParam recipientPKP pka ecdhPub kdfHA kdfSA)- kek <- either fail pure (deriveECDHKek kdfHA kdfSA sharedSecret param)- case aesKeyUnwrapRFC3394 kdfSA kek wrappedSessionKeyBytes of- Left err -> fail err- Right decoded -> pure decoded- EdDSAPubKey Ed25519 _ -> do- recipientSecretRaw <-- either fail pure (resolveX25519SecretRaw recipientPKP privateKey)- recipientSecret <-- either fail pure .- first show . CE.eitherCryptoError $- C25519.secretKey recipientSecretRaw- ephBytes <- either fail pure (normalizeX25519EphemeralPublic ephemeralBytes)- ephPub <-- either fail pure .- first show . CE.eitherCryptoError $- C25519.publicKey ephBytes- let sharedSecret = BA.convert (C25519.dh ephPub recipientSecret) :: B.ByteString- param <- either fail pure (buildECDHKDFParam recipientPKP pka ecdhPub kdfHA kdfSA)- let rfc6637Result =- do kek <- deriveECDHKek kdfHA kdfSA sharedSecret param- aesKeyUnwrapRFC3394 kdfSA kek wrappedSessionKeyBytes- case rfc6637Result of- Right decoded -> pure decoded- Left rfc6637Err -> do- recipientPublicRaw <- either fail pure (extractX25519RecipientPublic recipientPKP)- let kekX25519 = deriveX25519Kek ephBytes recipientPublicRaw sharedSecret- case aesKeyUnwrapRFC3394 AES128 kekX25519 wrappedSessionKeyBytes of- Right decoded -> pure decoded- Left x25519Err ->- fail- ("ECDH PKESKv6 Curve25519 unwrap failed (RFC6637: " ++- rfc6637Err ++ ", X25519: " ++ x25519Err ++ ")")- EdDSAPubKey Ed448 _ ->- fail- "ECDH PKESKv6 unwrap does not support Curve448Legacy recipients; use X448 PKESKv6 packets"- _ ->- fail- "ECDH PKESKv6 unwrap requires recipient ECDH public key to be ECDSA or X25519-compatible"- _ ->- fail- "ECDH PKESKv6 unwrap requires recipient PKPayload with ECDHPubKey parameters"-- ecdhUnwrapV6XDHRaw recipientCtx pka esk privateKeyRaw = do- recipientPKP <-- maybe- (fail- "X25519/X448 PKESKv6 unwrap requires recipient PKPayload context; use conduitDecrypt with DecryptWithKeyring or DecryptWithUnwrapCandidatesCallback")- pure- (pkeskRecipientPKPayload recipientCtx)- case pka of- X25519 -> v6X25519Unwrap recipientPKP (leftPadTo 32 privateKeyRaw) esk- X448 -> v6X448Unwrap recipientPKP (leftPadTo 56 privateKeyRaw) esk- _ ->- fail- ("X25519/X448 PKESKv6 unwrap only supports X25519/X448 packets; got " ++- show pka)-- v6X25519Unwrap recipientPKP recipientSecretRaw esk = do- recipientPublicRaw <- either fail pure (extractX25519RecipientPublic recipientPKP)- (ephemeralBytes, wrappedSessionKeyBytes) <-- either fail pure (parsePKESKv6ECDHEsk X25519 esk)- ephBytes <- either fail pure (normalizeX25519EphemeralPublic ephemeralBytes)- recipientSecret <-- either fail pure .- first show . CE.eitherCryptoError $- C25519.secretKey (leftPadTo 32 recipientSecretRaw)- ephPub <-- either fail pure .- first show . CE.eitherCryptoError $- C25519.publicKey ephBytes- let sharedSecret = BA.convert (C25519.dh ephPub recipientSecret) :: B.ByteString- kek = deriveX25519Kek ephBytes recipientPublicRaw sharedSecret- case aesKeyUnwrapRFC3394 AES128 kek wrappedSessionKeyBytes of- Left err -> fail err- Right decoded -> pure decoded-- extractX25519RecipientPublic recipientPKP =- case _pubkey recipientPKP of- EdDSAPubKey Ed25519 point ->- normalizeX25519EphemeralPublic (edPointBytes point)- ECDHPubKey (EdDSAPubKey Ed25519 point) _ _ ->- normalizeX25519EphemeralPublic (edPointBytes point)- other ->- Left- ("X25519 PKESKv6 unwrap requires an X25519 recipient public key, got " ++- show other)-- extractX448RecipientPublic recipientPKP =- case _pubkey recipientPKP of- EdDSAPubKey Ed448 point ->- normalizeX448EphemeralPublic (edPointBytes point)- ECDHPubKey (EdDSAPubKey Ed448 point) _ _ ->- normalizeX448EphemeralPublic (edPointBytes point)- other ->- Left- ("X448 PKESKv6 unwrap requires an X448 recipient public key, got " ++- show other)-- resolveX25519SecretRaw recipientPKP privateKey = do- recipientPublicRaw <- extractX25519RecipientPublic recipientPKP- let secretBE = leftPadTo 32 (i2osp (ECDSA.private_d privateKey))- candidates = [secretBE, B.reverse secretBE]- matchesCandidate candidate =- case CE.eitherCryptoError (C25519.secretKey candidate) of- Right sk ->- let derivedPub = BA.convert (C25519.toPublic sk) :: B.ByteString- in derivedPub == recipientPublicRaw- Left _ -> False- case filter matchesCandidate candidates of- (candidate:_) -> Right candidate- [] -> Right secretBE-- v6X448Unwrap recipientPKP recipientSecretRaw esk = do- recipientPublicRaw <- either fail pure (extractX448RecipientPublic recipientPKP)- (ephemeralBytes, wrappedSessionKeyBytes) <-- either fail pure (parsePKESKv6ECDHEsk X448 esk)- ephBytes <- either fail pure (normalizeX448EphemeralPublic ephemeralBytes)- recipientSecret <-- either fail pure .- first show . CE.eitherCryptoError $- C448.secretKey (leftPadTo 56 recipientSecretRaw)- ephPub <-- either fail pure .- first show . CE.eitherCryptoError $- C448.publicKey ephBytes- let sharedSecret = BA.convert (C448.dh ephPub recipientSecret) :: B.ByteString- kek = deriveX448Kek ephBytes recipientPublicRaw sharedSecret- case aesKeyUnwrapRFC3394 AES256 kek wrappedSessionKeyBytes of- Left err -> fail err- Right decoded -> pure decoded-- x25519UnwrapV3 recipientCtx mpis recipientSecretRaw = do- recipientPKP <-- maybe- (fail- "X25519 PKESKv3 unwrap requires recipient PKPayload context; use conduitDecrypt with DecryptWithKeyring or DecryptWithUnwrapCandidatesCallback")- pure- (pkeskRecipientPKPayload recipientCtx)- recipientPublicRaw <- either fail pure (extractX25519RecipientPublic recipientPKP)- (ephemeralBytes, eskBytes) <-- either fail pure (parseECDHPKESKMPIs mpis)- ephBytes <- either fail pure (normalizeX25519EphemeralPublic ephemeralBytes)- recipientSecret <-- either fail pure .- first show . CE.eitherCryptoError $- C25519.secretKey (leftPadTo 32 recipientSecretRaw)- ephPub <-- either fail pure .- first show . CE.eitherCryptoError $- C25519.publicKey ephBytes- let sharedSecret = BA.convert (C25519.dh ephPub recipientSecret) :: B.ByteString- kek9580 = deriveX25519Kek ephBytes recipientPublicRaw sharedSecret- -- RFC9580 interpretation: eskBytes = algo_byte || AES-KW(raw_session_key)- rfc9580Result = do- (sessionAlgorithm, wrappedKey) <- parsePKESKv3X25519EskBytes eskBytes- rawKey <- aesKeyUnwrapRFC3394 AES128 kek9580 wrappedKey- expectedLen <- symmetricKeyLength sessionAlgorithm- when (B.length rawKey /= expectedLen) $- Left- ("X25519 PKESKv3 unwrapped session key length mismatch for " ++- show sessionAlgorithm ++- ": expected " ++ show expectedLen ++ ", got " ++ show (B.length rawKey))- Right- (B.singleton (fromIntegral (fromFVal sessionAlgorithm)) <>- rawKey <>- checksum16Bytes rawKey)- case rfc9580Result of- Right result -> pure result- Left rfc9580Err ->- -- Fallback: legacy ECDH interpretation where the full eskBytes is- -- AES-KW(algo || key || checksum || padding). Try with the RFC9580- -- X25519 KEK and, when available, the RFC6637 ECDH KEK derived from- -- any ECDH parameters on the recipient public key.- let kekPairs = (kek9580, AES128) : x25519LegacyECDHKekCandidates recipientPKP sharedSecret- wrapped = LegacyECDHWrappedRFC3394Ciphertext eskBytes- candidates = candidateWrappedRFC3394CiphertextsForLegacyECDH wrapped- validResults =- [ encodeLegacyECDHSessionMaterial material- | (kek, kekSA) <- kekPairs- , candidate <- candidates- , Right decoded <-- [aesKeyUnwrapRFC3394 kekSA kek (unLegacyECDHWrappedRFC3394Ciphertext candidate)]- , Right material <- [parseLegacyECDHDecodedSessionMaterial decoded]- ]- in case validResults of- (result:_) -> pure result- [] ->- fail- ("X25519 PKESKv3 unwrap failed (RFC9580: " ++- rfc9580Err ++- "; legacy ECDH-style fallback also failed)")-- -- | Derive RFC6637 ECDH KEK candidates for legacy X25519 PKESKv3 fallback.- -- Returns @(kek, kekAlgorithm)@ pairs for each plausible ECDH KDF- -- parameterisation found on the recipient public key.- x25519LegacyECDHKekCandidates ::- SomePKPayload- -> B.ByteString- -> [(B.ByteString, SymmetricAlgorithm)]- x25519LegacyECDHKekCandidates pkPayload sharedSecret =- case _pubkey pkPayload of- ECDHPubKey ecdhPub kdfHA kdfSA ->- [ (kek, kdfSA)- | Right param <- [buildECDHKDFParam pkPayload X25519 ecdhPub kdfHA kdfSA]- , Right kek <- [deriveECDHKek kdfHA kdfSA sharedSecret param]- ]- _ -> []--rsaModulusBytes :: RSATypes.PrivateKey -> Int-rsaModulusBytes (RSATypes.PrivateKey (RSATypes.PublicKey sizeField _ _) _ _ _ _ _ _) =- if sizeField > 512- then (sizeField + 7) `div` 8- else sizeField--edPointBytes :: EdPoint -> B.ByteString-edPointBytes (PrefixedNativeEPoint (EPoint x)) = i2osp x-edPointBytes (NativeEPoint (EPoint x)) = i2osp x--parseECDHPKESKMPIs :: NonEmpty MPI -> Either String (B.ByteString, B.ByteString)-parseECDHPKESKMPIs (ephemeralMPI :| [wrappedMPI]) =- Right (i2osp (unMPI ephemeralMPI), i2osp (unMPI wrappedMPI))-parseECDHPKESKMPIs _ =- Left "ECDH PKESK must contain exactly two MPIs (ephemeral key, wrapped session key)"--newtype LegacyECDHWrappedRFC3394Ciphertext =- LegacyECDHWrappedRFC3394Ciphertext- { unLegacyECDHWrappedRFC3394Ciphertext :: B.ByteString- }- deriving (Eq)--newtype LegacyECDHSessionKey = LegacyECDHSessionKey { unLegacyECDHSessionKey :: B.ByteString }--newtype LegacyECDHSessionPadding = LegacyECDHSessionPadding { unLegacyECDHSessionPadding :: B.ByteString }--data LegacyECDHDecodedSessionMaterial =- LegacyECDHDecodedSessionMaterial- { legacyECDHSessionAlgorithm :: SymmetricAlgorithm- , legacyECDHSessionKey :: LegacyECDHSessionKey- , legacyECDHSessionPadding :: LegacyECDHSessionPadding- }--candidateWrappedRFC3394CiphertextsForLegacyECDH ::- LegacyECDHWrappedRFC3394Ciphertext -> [LegacyECDHWrappedRFC3394Ciphertext]-candidateWrappedRFC3394CiphertextsForLegacyECDH wrapped =- let observedLen = B.length (unLegacyECDHWrappedRFC3394Ciphertext wrapped)- plausibleWrappedLens = legacyECDHRFC3394WrappedLengths- reconstructed =- [ LegacyECDHWrappedRFC3394Ciphertext- (if observedLen == targetLen- then unLegacyECDHWrappedRFC3394Ciphertext wrapped- else leftPadTo targetLen (unLegacyECDHWrappedRFC3394Ciphertext wrapped))- | targetLen <- plausibleWrappedLens- , targetLen >= observedLen- ]- in nub (wrapped : reconstructed)--legacyECDHRFC3394WrappedLengths :: [Int]-legacyECDHRFC3394WrappedLengths =- map legacyRFC3394WrappedLenForKeyLen [16, 24, 32]- where- legacyRFC3394WrappedLenForKeyLen keyLen =- let encodedLen = 1 + keyLen + 2- paddedLen = ((encodedLen + 7) `div` 8) * 8- in paddedLen + 8--parseLegacyECDHDecodedSessionMaterial ::- B.ByteString -> Either String LegacyECDHDecodedSessionMaterial-parseLegacyECDHDecodedSessionMaterial decoded = do- when (B.length decoded < 3) $- Left "legacy ECDH decoded session material is too short"- let sessionAlgorithm = toFVal (B.head decoded)- sessionKeyLen <- symmetricKeyLength sessionAlgorithm- let payload = B.tail decoded- when (B.length payload < sessionKeyLen + 2) $- Left "legacy ECDH decoded session material does not contain full key and checksum"- let (sessionKey, checksumAndPad) = B.splitAt sessionKeyLen payload- (checksumBytes, padBytes) = B.splitAt 2 checksumAndPad- expectedChecksum =- fromIntegral (B.index checksumBytes 0) `shiftL` 8 +- fromIntegral (B.index checksumBytes 1)- actualChecksum = checksum16 sessionKey- when (actualChecksum /= expectedChecksum) $- Left "legacy ECDH decoded session-key checksum mismatch"- if B.null padBytes || B.all (== 0) padBytes- then- Right- (LegacyECDHDecodedSessionMaterial- sessionAlgorithm- (LegacyECDHSessionKey sessionKey)- (LegacyECDHSessionPadding padBytes))- else do- validatePKCS7Padding padBytes- Right- (LegacyECDHDecodedSessionMaterial- sessionAlgorithm- (LegacyECDHSessionKey sessionKey)- (LegacyECDHSessionPadding padBytes))--encodeLegacyECDHSessionMaterial :: LegacyECDHDecodedSessionMaterial -> B.ByteString-encodeLegacyECDHSessionMaterial- (LegacyECDHDecodedSessionMaterial sessionAlgorithm (LegacyECDHSessionKey sessionKey) _) =- B.singleton (fromIntegral (fromFVal sessionAlgorithm)) <>- sessionKey <>- checksum16Bytes sessionKey--parsePKESKv6ECDHEsk :: PubKeyAlgorithm -> B.ByteString -> Either String (B.ByteString, B.ByteString)-parsePKESKv6ECDHEsk pka esk- | pka == X25519 =- case parseFixedEphemeralWithWrappedLen 32 esk of- Right parsed -> Right parsed- Left _ -> parseLenPrefixedEphemeral 32 esk- | pka == X448 =- case parseFixedEphemeralWithWrappedLen 56 esk of- Right parsed -> Right parsed- Left _ -> parseLenPrefixedEphemeral 56 esk- | B.length esk < 33 =- Left "PKESKv6 ECDH ESK is too short"- | otherwise =- let withLen =- let ephLen = fromIntegral (B.head esk)- rest = B.tail esk- in if ephLen > 0 && B.length rest > ephLen- then- let (eph, wrapped) = B.splitAt ephLen rest- in if validWrappedPayload wrapped- then Just (eph, wrapped)- else Nothing- else Nothing- fixed32WithWrappedLen = parseFixedEphemeralWithWrappedLenMaybe 32 esk- fixed32 =- let (eph, wrapped) = B.splitAt 32 esk- in if validWrappedPayload wrapped- then Just (eph, wrapped)- else Nothing- mpiWithWrappedLen =- if B.length esk >= 4- then- let mpiBits = fromIntegral (B.index esk 0) `shiftL` 8 + fromIntegral (B.index esk 1)- mpiLen = (mpiBits + 7) `div` 8- rest = B.drop (2 + mpiLen) esk- in if mpiLen > 0 && B.length esk > 2 + mpiLen && not (B.null rest)- then- let eph = B.take mpiLen (B.drop 2 esk)- wrappedLen = fromIntegral (B.head rest)- wrapped = B.tail rest- in if wrappedLen == B.length wrapped && validWrappedPayload wrapped- then Just (eph, wrapped)- else Nothing- else Nothing- else Nothing- in case fixed32WithWrappedLen <|> withLen <|> fixed32 <|> mpiWithWrappedLen of- Just x -> Right x- Nothing ->- Left- "PKESKv6 ECDH ESK could not be parsed as {ephemeral32||len||wrapped}, {len||ephemeral||wrapped}, {ephemeral32||wrapped}, or {mpi(ephemeral)||len||wrapped}"- where- validWrappedPayload wrapped = B.length wrapped >= 24 && B.length wrapped `mod` 8 == 0-- parseFixedEphemeralWithWrappedLenMaybe ephLen payload =- let (eph, rest) = B.splitAt ephLen payload- in if B.length rest >= 2- then- let wrappedLen = fromIntegral (B.head rest)- wrapped = B.tail rest- in if wrappedLen == B.length wrapped && validWrappedPayload wrapped- then Just (eph, wrapped)- else Nothing- else Nothing-- parseFixedEphemeralWithWrappedLen ephLen payload =- maybe- (Left- ("PKESKv6 XDH ESK expected {ephemeral" ++- show ephLen ++ "||len||wrapped} framing"))- Right- (parseFixedEphemeralWithWrappedLenMaybe ephLen payload)-- parseLenPrefixedEphemeral expectedLen payload =- if B.null payload- then Left "PKESKv6 XDH ESK is empty"- else- let ephLen = fromIntegral (B.head payload)- rest = B.tail payload- in if ephLen == expectedLen && B.length rest > ephLen- then- let (eph, wrapped) = B.splitAt ephLen rest- in if validWrappedPayload wrapped- then Right (eph, wrapped)- else Left "PKESKv6 XDH ESK wrapped payload has invalid length"- else- Left- ("PKESKv6 XDH ESK expected " ++- show expectedLen ++ "-octet ephemeral value")--normalizeX25519EphemeralPublic :: B.ByteString -> Either String B.ByteString-normalizeX25519EphemeralPublic =- normalizeMontgomeryPublic- 32- "invalid X25519 ephemeral public key length/prefix: "--normalizeX448EphemeralPublic :: B.ByteString -> Either String B.ByteString-normalizeX448EphemeralPublic =- normalizeMontgomeryPublic- 56- "invalid X448 ephemeral public key length/prefix: "--parseUncompressedPointForCurve :: MonadFail m => ECCT.Curve -> B.ByteString -> m ECCT.Point-parseUncompressedPointForCurve curve bs- | B.length bs < 1 = fail "ECDH ephemeral point is empty"- | Just expectedLen <- expectedUncompressedPointLength curve- , B.length bs /= expectedLen =- fail- ("ECDH ephemeral point has invalid length for recipient curve: expected " ++- show expectedLen ++ ", got " ++ show (B.length bs))- | B.head bs /= 0x04 = fail "ECDH ephemeral point must be uncompressed (0x04)"- | otherwise =- let xy = B.tail bs- in if odd (B.length xy)- then fail "ECDH ephemeral point has malformed coordinate length"- else- let (xb, yb) = B.splitAt (B.length xy `div` 2) xy- in pure (ECCT.Point (os2ip xb) (os2ip yb))--expectedUncompressedPointLength :: ECCT.Curve -> Maybe Int-expectedUncompressedPointLength curve- | curve == ECCT.getCurveByName ECCT.SEC_p256r1 = Just 65- | curve == ECCT.getCurveByName ECCT.SEC_p384r1 = Just 97- | curve == ECCT.getCurveByName ECCT.SEC_p521r1 = Just 133- | otherwise = Nothing--deriveX25519Kek :: B.ByteString -> B.ByteString -> B.ByteString -> B.ByteString-deriveX25519Kek ephemeralPublic recipientPublic sharedSecret =- let ikm = ephemeralPublic <> recipientPublic <> sharedSecret- prk = extract @CHA.SHA256 B.empty ikm- info :: B.ByteString- info = "OpenPGP X25519"- okm :: B.ByteString- okm = expand @CHA.SHA256 prk info 16- in okm--deriveX448Kek :: B.ByteString -> B.ByteString -> B.ByteString -> B.ByteString-deriveX448Kek ephemeralPublic recipientPublic sharedSecret =- let ikm = ephemeralPublic <> recipientPublic <> sharedSecret- prk = extract @CHA.SHA512 B.empty ikm- info :: B.ByteString- info = "OpenPGP X448"- okm :: B.ByteString- okm = expand @CHA.SHA512 prk info 32- in okm--aesKeyUnwrapRFC3394 ::- SymmetricAlgorithm -> B.ByteString -> B.ByteString -> Either String B.ByteString-aesKeyUnwrapRFC3394 sa kek wrapped =- withAESCipher "ECDH PKESK currently supports AES KEK algorithms only" sa kek unwrapWithCipher- where- unwrapWithCipher ::- CCT.BlockCipher cipher- => cipher- -> Either String B.ByteString- unwrapWithCipher cipher = do- when (B.length wrapped < 24 || B.length wrapped `mod` 8 /= 0) $- Left "ECDH wrapped session key must be at least 24 octets and a multiple of 8"- let (a0, rBytes) = B.splitAt 8 wrapped- rs = chunksOf8 rBytes- when (length rs < 2) $- Left "ECDH wrapped session key must contain at least two 64-bit blocks"- (aFinal, rFinal) <- unwrapRounds cipher a0 rs- when (aFinal /= B.replicate 8 0xA6) $- Left "ECDH wrapped session key integrity check failed"- Right (B.concat rFinal)-- unwrapRounds ::- CCT.BlockCipher cipher- => cipher- -> B.ByteString- -> [B.ByteString]- -> Either String (B.ByteString, [B.ByteString])- unwrapRounds cipher aInit rsInit = goJ 5 aInit rsInit- where- n = length rsInit- goJ j aState rsState- | j < 0 = Right (aState, rsState)- | otherwise = do- (a', rs') <- goI n aState rsState- goJ (j - 1) a' rs'- where- goI i aCurrent rsCurrent- | i <= 0 = Right (aCurrent, rsCurrent)- | otherwise = do- 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- rsNext = (ix (i - 1) .~ rNext) rsCurrent- goI (i - 1) aNext rsNext--chunksOf8 :: B.ByteString -> [B.ByteString]-chunksOf8 bs- | B.null bs = []- | otherwise =- let (h, t) = B.splitAt 8 bs- in h : chunksOf8 t--xorBS :: B.ByteString -> B.ByteString -> B.ByteString-xorBS a b = B.pack (B.zipWith xor a b)--decodePKESKSessionKey ::- Maybe SymmetricAlgorithm- -> B.ByteString- -> Either String (SymmetricAlgorithm, B.ByteString)-decodePKESKSessionKey expectedSymAlgo encodedSessionKey =- case decodeOpenPGPEncodedSessionKey encodedSessionKey of- Right (symalgo, sessionKey) ->- case expectedSymAlgo of- Just expected | expected /= symalgo ->- Left "Decrypted PKESK symmetric algorithm does not match payload"- _ -> Right (symalgo, sessionKey)- Left decodeErr ->- case expectedSymAlgo of- Nothing ->- Left- ("PKESK session key material must be OpenPGP encoded when payload algorithm is unknown: " ++- renderEncodedSessionKeyError decodeErr)- Just expected -> do- expectedLen <- symmetricKeyLength expected- case decodeExpectedRawOrPaddedSessionKey expected expectedLen encodedSessionKey of- Left err -> Left err- Right sessionKey -> Right (expected, sessionKey)--parsePKESKv3X25519EskBytes ::- B.ByteString -> Either String (SymmetricAlgorithm, B.ByteString)-parsePKESKv3X25519EskBytes eskBytes = do- when (B.length eskBytes < 2) $- Left "PKESKv3 X25519 ESK field is too short"- let sessionAlgorithm = toFVal (B.head eskBytes)- wrappedSessionKeyBytes = B.tail eskBytes- when (sessionAlgorithm `notElem` [AES128, AES192, AES256]) $- Left "PKESKv3 X25519 ESK field uses unsupported symmetric algorithm"- when (B.length wrappedSessionKeyBytes < 24 || B.length wrappedSessionKeyBytes `mod` 8 /= 0) $- Left "PKESKv3 X25519 wrapped session key must be at least 24 octets and a multiple of 8"- pure (sessionAlgorithm, wrappedSessionKeyBytes)--decodeExpectedRawOrPaddedSessionKey ::- SymmetricAlgorithm -> Int -> B.ByteString -> Either String B.ByteString-decodeExpectedRawOrPaddedSessionKey expected expectedLen encodedSessionKey- | B.length encodedSessionKey == expectedLen = Right encodedSessionKey- | otherwise =- case decodeV6PaddedSessionKeyWithoutAlgo expectedLen encodedSessionKey of- Right sessionKey -> Right sessionKey- Left _ ->- case decodeV6PaddedSessionKeyWithAlgo expected expectedLen encodedSessionKey of- Right sessionKey -> Right sessionKey- Left _ ->- Left "PKESK raw session key length does not match payload algorithm"--decodeV6PaddedSessionKeyWithoutAlgo :: Int -> B.ByteString -> Either String B.ByteString-decodeV6PaddedSessionKeyWithoutAlgo expectedLen encodedSessionKey = do- when (B.length encodedSessionKey < expectedLen + 2) $- Left "v6 ECDH decoded session material is too short"- let (sessionKey, rest) = B.splitAt expectedLen encodedSessionKey- (checksumBytes, padBytes) = B.splitAt 2 rest- expectedChecksum =- fromIntegral (B.index checksumBytes 0) `shiftL` 8 +- fromIntegral (B.index checksumBytes 1)- actualChecksum = checksum16 sessionKey- when (actualChecksum /= expectedChecksum) $- Left "v6 ECDH decoded session-key checksum mismatch"- validatePKCS7Padding padBytes- Right sessionKey--decodeV6PaddedSessionKeyWithAlgo ::- SymmetricAlgorithm -> Int -> B.ByteString -> Either String B.ByteString-decodeV6PaddedSessionKeyWithAlgo expected expectedLen encodedSessionKey = do- when (B.length encodedSessionKey < expectedLen + 3) $- Left "v6 ECDH decoded session material with algorithm octet is too short"- let algOctet = B.head encodedSessionKey- when (toFVal algOctet /= expected) $- Left "v6 ECDH decoded session material algorithm mismatch"- let rest = B.tail encodedSessionKey- (sessionKey, checksumAndPad) = B.splitAt expectedLen rest- (checksumBytes, padBytes) = B.splitAt 2 checksumAndPad- expectedChecksum =- fromIntegral (B.index checksumBytes 0) `shiftL` 8 +- fromIntegral (B.index checksumBytes 1)- actualChecksum = checksum16 sessionKey- when (actualChecksum /= expectedChecksum) $- Left "v6 ECDH decoded session-key checksum mismatch"- validatePKCS7Padding padBytes- Right sessionKey--validatePKCS7Padding :: B.ByteString -> Either String ()-validatePKCS7Padding padBytes- | B.null padBytes = Right ()- | otherwise = do- let padLen = fromIntegral (B.last padBytes) :: Int- when (padLen <= 0 || padLen > 8 || B.length padBytes /= padLen) $- Left "v6 ECDH decoded session material has invalid PKCS#7-style padding length"- when (B.any (/= fromIntegral padLen) padBytes) $- Left "v6 ECDH decoded session material has invalid PKCS#7-style padding bytes"--symmetricKeyLength :: SymmetricAlgorithm -> Either String Int-symmetricKeyLength = first renderCipherError . keySize--checksum16 :: B.ByteString -> Word16-checksum16 =- fromIntegral .- B.foldl' (\acc octet -> (acc + fromIntegral octet) `mod` (65536 :: Integer)) 0--checksum16Bytes :: B.ByteString -> B.ByteString-checksum16Bytes sessionKey =- B.pack [fromIntegral (chk `shiftR` 8), fromIntegral chk]+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE TypeApplications #-}++module Data.Conduit.OpenPGP.Decrypt+ ( conduitDecrypt+ , conduitDecryptWithReport+ , DecryptOptions (..)+ , DecryptKeyResolution (..)+ , PKESKRecipientKey (..)+ , PKESKAttemptFailureKind (..)+ , PKESKAttemptFailure (..)+ , DecryptOutcome (..)+ , DecryptReport (..)+ , DecryptSessionKeyResolutionReport (..)+ , DecryptSessionKeyResolutionPath (..)+ , PKESKResolverAttempt (..)+ , PKESKResolverAttemptAction (..)+ , decryptSEIPDv2Payload+ ) where++import Control.Applicative ((<|>))+import Control.Exception (SomeException, displayException, try)+import Control.Lens (ix, (.~))+import Control.Monad (when)+import Control.Monad.IO.Class (MonadIO (..))+import Control.Monad.IO.Unlift (MonadUnliftIO)+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT)+import Control.Monad.Trans.Resource (MonadResource, MonadThrow)+import qualified Crypto.Error as CE+import qualified Crypto.Hash as CH+import qualified Crypto.Hash.Algorithms as CHA+import Crypto.KDF.HKDF (expand, extract)+import Crypto.Number.Serialize (i2osp, os2ip)+import qualified Crypto.PubKey.Curve25519 as C25519+import qualified Crypto.PubKey.Curve448 as C448+import qualified Crypto.PubKey.ECC.DH as ECCDH+import qualified Crypto.PubKey.ECC.ECDSA as ECDSA+import qualified Crypto.PubKey.ECC.Types as ECCT+import qualified Crypto.PubKey.RSA.PKCS15 as P15+import qualified Crypto.PubKey.RSA.Types as RSATypes+import Data.Bifunctor (first)+import Data.Binary (get)+import Data.Binary.Put (putWord64be, runPut)+import Data.Bits (countLeadingZeros, shiftL, shiftR, xor)+import qualified Data.ByteArray as BA+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import Data.Conduit+import qualified Data.Conduit.Binary as CB+import qualified Data.Conduit.List as CL+import Data.Conduit.Serialization.Binary (conduitGet)+import Data.IORef+ ( IORef+ , modifyIORef'+ , newIORef+ , readIORef+ , writeIORef+ )+import qualified Data.IxSet.Typed as IxSet+import Data.List (intercalate, nub)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Maybe (catMaybes, isNothing, mapMaybe)+import Data.Word (Word16, Word64, Word8)+import qualified "crypton" Crypto.Cipher.Types as CCT++import Codec.Encryption.OpenPGP.BlockCipher+ ( keySize+ , renderCipherError+ )+import Codec.Encryption.OpenPGP.CFB+ ( calculateMDC+ , decryptOpenPGPCfb+ , decryptPreservingNonce+ , validateSEIPD1MDC+ )+import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)+import Codec.Encryption.OpenPGP.Internal (leftPadTo)+import Codec.Encryption.OpenPGP.Internal.CryptoAES+ ( withAESCipher+ )+import Codec.Encryption.OpenPGP.Internal.CryptoECDH+ ( buildECDHKDFParam+ , deriveECDHKek+ , normalizeMontgomeryPublic+ )+import Codec.Encryption.OpenPGP.Internal.CryptoSEIPDv2+ ( aeadModeAndNonceSizeForSEIPDv2+ , decryptSKESK6SessionKey+ , deriveSKESK6KEK+ , seipdv2SymmetricKeySize+ )+import Codec.Encryption.OpenPGP.Internal.RFC7253OCB+ ( decryptWithOCBRFC7253With+ )+import Codec.Encryption.OpenPGP.Policy+ ( DecryptPolicy (..)+ , defaultDecryptPolicy+ , validateTable30PolicyForRecipient+ )+import Codec.Encryption.OpenPGP.S2K+ ( S2KError (..)+ , decodeOpenPGPEncodedSessionKey+ , renderEncodedSessionKeyError+ , renderS2KError+ , skesk2Key+ , skesk2SessionKey+ , string2Key+ )+import Codec.Encryption.OpenPGP.SecretKey (decryptPrivateKey)+import Codec.Encryption.OpenPGP.Types+import Data.Conduit.OpenPGP.Compression (conduitDecompress)+import Data.Conduit.OpenPGP.Keyring.Instances ()++data RecursorState+ = RecursorState+ { _depth :: Int+ , _pendingESKs :: [PendingESK]+ , _lastNonce :: Maybe B.ByteString+ , _lastClearText :: Maybe B.ByteString+ , _decryptPolicy :: DecryptPolicy+ }+ deriving (Eq, Show)++def :: RecursorState+def = RecursorState 0 [] Nothing Nothing defaultDecryptPolicy++data DecryptStreamPhase+ = ActiveDecryptPhase+ | FinishedDecryptPhase+ | MalformedDecryptPhase++data DecryptStreamState (phase :: DecryptStreamPhase) where+ ActiveDecryptState+ :: RecursorState+ -> DecryptStreamState 'ActiveDecryptPhase+ FinishedDecryptState+ :: RecursorState+ -> Bool+ -> DecryptStreamState 'FinishedDecryptPhase+ MalformedDecryptState+ :: RecursorState+ -> String+ -> DecryptStreamState 'MalformedDecryptPhase++data SomeDecryptStreamState where+ SomeDecryptStreamState+ :: DecryptStreamState phase+ -> SomeDecryptStreamState++data PendingESK+ = PendingPKESK PKESKPayload+ | PendingSKESK SKESKPayload+ deriving (Eq, Show)++data EncryptedPayloadVersion+ = LegacyEncryptedPayloadVersion+ | SEIPDv2EncryptedPayloadVersion++data EncryptedPayloadFlavor (v :: EncryptedPayloadVersion) where+ LegacyEncryptedPayload+ :: EncryptedPayloadFlavor 'LegacyEncryptedPayloadVersion+ SEIPDv2EncryptedPayload+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> EncryptedPayloadFlavor 'SEIPDv2EncryptedPayloadVersion++type InputCallback m = String -> m BL.ByteString++data PKESKRecipientKey+ = PKESKRecipientKey+ { pkeskRecipientPKPayload :: Maybe SomePKPayload+ {- ^ Public key payload for the recipient. Required for X25519, X448,+ and ECDH unwrap paths; may be 'Nothing' for RSA.+ -}+ , pkeskRecipientSKey :: SKey+ -- ^ Corresponding secret key.+ }++data PKESKAttemptFailure+ = PKESKAttemptFailure+ { pkeskAttemptFailureKeyContext+ :: Maybe (KeyVersion, PubKeyAlgorithm)+ , pkeskAttemptFailureKind :: PKESKAttemptFailureKind+ , pkeskAttemptFailureReason :: String+ }+ deriving (Eq, Show)++data PKESKAttemptFailureKind+ = PKESKAttemptUnwrapFailed+ | PKESKAttemptSessionMaterialDecodeFailed+ deriving (Eq, Show)++data PKESKResolverError+ = ResolverPolicyDenied String+ | ResolverBackendUnavailable String+ | ResolverInvalidResponse String+ deriving (Eq, Show)++data PKESKResolveRequest+ = PKESKResolveRequest+ { reqPKESK :: PKESKPayload+ , reqProbePacket :: Pkt+ , reqIsWildcardRecipient :: Bool+ , reqAttemptIndex :: Int+ , reqPreviousFailures :: [PKESKAttemptFailure]+ }+ deriving (Eq, Show)++data PKESKResolveAction+ = ResolveWith PKESKRecipientKey+ | ResolveSkip+ | ResolveExhausted+ | ResolveFail PKESKResolverError++type PKESKResolver m =+ PKESKResolveRequest -> m PKESKResolveAction++{- | Build a stateful 'PKESKResolver' that iterates over a pre-populated+candidate list. The returned resolver yields candidates in order and+returns 'ResolveExhausted' once the list is depleted.+| How to resolve secret keys for PKESK-encrypted messages.+-}+data DecryptKeyResolution+ = {- | Do not attempt PKESK decryption; fall through to manual session-key+ input via the passphrase callback instead.+ -}+ DecryptWithoutPKESK+ | {- | Resolve secret keys from a 'SecretKeyring'. Only unencrypted secret+ keys (not passphrase-protected) are used. For passphrase-protected keys,+ use 'DecryptWithKeyringAndPassphrase'.+ -}+ DecryptWithKeyring SecretKeyring+ | {- | Resolve secret keys from a 'SecretKeyring', unlocking passphrase-+ protected keys using the provided callback. The callback receives the+ public key payload and returns the passphrase, or 'Nothing' to skip.+ -}+ DecryptWithKeyringAndPassphrase+ SecretKeyring+ (SomePKPayload -> IO (Maybe BL.ByteString))+ | {- | Preferred callback form for non-keyring key material. The callback+ receives a typed key identifier (8-octet key ID, fingerprint, or+ wildcard) plus the packet public-key algorithm, then returns all+ matching candidates in priority order. hOpenPGP iterates candidates+ deterministically without re-calling the callback for each retry.+ -}+ DecryptWithUnwrapCandidatesCallback+ (KeyIdentifier -> PubKeyAlgorithm -> IO [PKESKRecipientKey])++{- | Canonical decrypt configuration.++Most callers should prefer 'conduitDecrypt' and set:++* 'decryptOptionsKeyResolution' to 'DecryptWithKeyring' or 'DecryptWithKeyringAndPassphrase'+* 'decryptOptionsPolicy' to strict ('defaultDecryptPolicy') or lenient+* 'decryptOptionsPassphraseCallback' for SKESK passphrase lookup+-}+data DecryptOptions+ = DecryptOptions+ { decryptOptionsKeyResolution :: DecryptKeyResolution+ , decryptOptionsPolicy :: DecryptPolicy+ , decryptOptionsPassphraseCallback :: InputCallback IO+ }++{- | Outcome of a checked decrypt conduit run.++Use 'conduitDecrypt' to obtain this value. Because+'Data.Conduit..|' preserves the+/rightmost/ conduit's return value, callers who also need the decrypted+packet stream should use 'Data.Conduit.fuseBoth':++@+(outcome, pkts) \<- runConduit $ source .| fuseBoth (conduitDecrypt opts) CL.consume+@+-}+data DecryptOutcome+ = {- | The integrity-terminating marker (MDC or SEIPD v2 final AEAD tag)+ was seen and no further packets arrived. The message was+ well-formed end-to-end.+ -}+ DecryptClean+ | {- | The input stream ended before any integrity-terminating marker was+ seen. The ciphertext was incomplete.+ -}+ DecryptTruncated+ | {- | An integrity-terminating marker was seen, but additional packets+ followed it. Only possible with 'lenientDecryptPolicy' (strict+ policy reports 'DecryptMalformedStructure' instead). The trailing+ packets were forwarded downstream unchanged.+ -}+ DecryptTrailingData+ | {- | A structural packet-sequencing violation was detected. The+ 'String' describes the specific violation:++ * PKESK version does not match the SEIPD version (e.g. a v6 PKESK+ preceding a SEIPDv1 payload, or a v4 SKESK preceding a SEIPDv2+ payload).++ * ESK packets arrived in the wrong order relative to the encrypted+ data packet (e.g. a literal-data packet appeared between a PKESK+ and the SEIPD it was intended to protect).++ * A packet arrived after the message integrity boundary (trailing+ data). Under 'defaultDecryptPolicy' this is reported here; under+ 'lenientDecryptPolicy' it is reported as 'DecryptTrailingData'+ instead.+ -}+ DecryptMalformedStructure String+ deriving (Eq, Show)++data DecryptSessionKeyResolutionPath+ = DecryptResolvedViaSKESK+ | DecryptResolvedViaPKESK+ | DecryptResolvedViaManualPKESKInput+ deriving (Eq, Show)++data PKESKResolverAttemptAction+ = ResolverAttemptResolveWith (Maybe (KeyVersion, PubKeyAlgorithm))+ | ResolverAttemptSkip+ | ResolverAttemptExhausted+ | ResolverAttemptFail PKESKResolverError+ deriving (Eq, Show)++data PKESKResolverAttempt+ = PKESKResolverAttempt+ { pkeskResolverAttemptPreviousFailures :: [PKESKAttemptFailure]+ -- ^ Failures from earlier attempts on the same PKESK packet.+ , pkeskResolverAttemptAction :: PKESKResolverAttemptAction+ }+ deriving (Eq, Show)++data DecryptSessionKeyResolutionReport+ = DecryptSessionKeyResolutionReport+ { decryptSessionResolutionPath :: DecryptSessionKeyResolutionPath+ , decryptSessionResolutionSKESKErrors :: [String]+ , decryptSessionResolutionPKESKErrors :: [String]+ , decryptSessionResolutionResolverAttempts+ :: [PKESKResolverAttempt]+ }+ deriving (Eq, Show)++data DecryptReport+ = DecryptReport+ { decryptReportOutcome :: DecryptOutcome+ , decryptReportSessionKeyResolutions+ :: [DecryptSessionKeyResolutionReport]+ }+ deriving (Eq, Show)++-- | AEAD decryption context (Reader monad eliminates parameter threading)+data AEADDecryptContext cipher+ = AEADDecryptContext+ { aeadMode :: CCT.AEADMode+ , aeadInfo :: B.ByteString+ , aeadChunkSize :: Word8+ , aeadNoncePrefix :: B.ByteString+ , aeadCipher :: cipher+ }++-- | ReaderT wrapper for AEAD decryption computations+type AEADDecrypt cipher =+ ReaderT (AEADDecryptContext cipher) (Either String)++conduitDecrypt+ :: (MonadFail m, MonadResource m, MonadThrow m, MonadUnliftIO m)+ => DecryptOptions+ -> ConduitT Pkt Pkt m DecryptOutcome+conduitDecrypt opts =+ decryptReportOutcome <$> conduitDecryptWithReport opts++conduitDecryptWithReport+ :: (MonadFail m, MonadResource m, MonadThrow m, MonadUnliftIO m)+ => DecryptOptions+ -> ConduitT Pkt Pkt m DecryptReport+conduitDecryptWithReport opts = do+ reportRef <- liftIO (newIORef [])+ resolver <-+ liftIO (buildDecryptResolver (decryptOptionsKeyResolution opts))+ let allowManualPKESKPrompt =+ case decryptOptionsKeyResolution opts of+ DecryptWithoutPKESK -> True+ _ -> False+ outcome <-+ conduitDecryptChecked'+ (def {_decryptPolicy = decryptOptionsPolicy opts})+ allowManualPKESKPrompt+ resolver+ (decryptOptionsPassphraseCallback opts)+ (Just reportRef)+ resolutions <- reverse <$> liftIO (readIORef reportRef)+ pure+ DecryptReport+ { decryptReportOutcome = outcome+ , decryptReportSessionKeyResolutions = resolutions+ }++-- | Build an internal 'PKESKResolver' from the public 'DecryptKeyResolution'.+buildDecryptResolver+ :: DecryptKeyResolution -> IO (PKESKResolver IO)+buildDecryptResolver DecryptWithoutPKESK =+ pure (\_ -> pure ResolveExhausted)+buildDecryptResolver (DecryptWithKeyring kr) =+ buildKeyringResolver kr Nothing+buildDecryptResolver (DecryptWithKeyringAndPassphrase kr cb) =+ buildKeyringResolver kr (Just cb)+buildDecryptResolver (DecryptWithUnwrapCandidatesCallback cb) =+ buildUnwrapCandidatesResolver cb++-- | Build a stateful resolver that looks up keys from a 'SecretKeyring'.+buildKeyringResolver+ :: SecretKeyring+ -> Maybe (SomePKPayload -> IO (Maybe BL.ByteString))+ -> IO (PKESKResolver IO)+buildKeyringResolver kr maybePassphraseCb = do+ -- Tracks (last PKESK, remaining wildcard candidates once initialized).+ stateRef <-+ newIORef+ ( Nothing :: Maybe PKESKPayload+ , Nothing :: Maybe [PKESKRecipientKey]+ , [] :: [(Pkt, [PKESKRecipientKey])]+ )+ pure $ \req -> do+ let pkesk = reqPKESK req+ probe = reqProbePacket req+ (lastPKESK, wildcardState, exactCache) <- readIORef stateRef+ let freshPKESK = Just pkesk /= lastPKESK+ when freshPKESK $ writeIORef stateRef (Just pkesk, Nothing, [])+ let wc = if freshPKESK then Nothing else wildcardState+ ec = if freshPKESK then [] else exactCache+ case extractProbeKeyIdentifier probe of+ KeyIdentifierWildcard -> do+ -- Wildcard probe: iterate all keys+ candidates <- case wc of+ Nothing -> keyringCandidates (IxSet.toList kr)+ Just cs -> pure cs+ case candidates of+ [] -> do+ writeIORef stateRef (Just pkesk, Just [], ec)+ pure ResolveExhausted+ (rk : rest) -> do+ writeIORef stateRef (Just pkesk, Just rest, ec)+ pure (ResolveWith rk)+ keyIdentifier -> do+ -- Exact probe: direct lookup by key ID or fingerprint.+ candidates <- case lookup probe ec of+ Just cs -> pure cs+ Nothing -> do+ let matchingTKs = matchingTKsForRecipient keyIdentifier+ cs <- keyringCandidates matchingTKs+ writeIORef+ stateRef+ ( Just pkesk+ , wc+ , (probe, cs) : filter ((/= probe) . fst) ec+ )+ pure cs+ let priorFailures = length (reqPreviousFailures req)+ case drop priorFailures candidates of+ [] -> pure ResolveSkip+ (rk : _) -> pure (ResolveWith rk)+ where+ matchingTKsForRecipient :: KeyIdentifier -> [TK 'SecretTK]+ matchingTKsForRecipient keyIdentifier =+ case keyIdentifier of+ KeyIdentifierWildcard -> IxSet.toList kr+ KeyIdentifierEightOctet rid -> IxSet.toList (kr IxSet.@= rid)+ KeyIdentifierFingerprint rid ->+ let indexedMatches = IxSet.toList (kr IxSet.@= rid)+ in if null indexedMatches+ then filter (tkMatchesRecipientFingerprint rid) (IxSet.toList kr)+ else indexedMatches++ tkMatchesRecipientFingerprint+ :: Fingerprint -> TK 'SecretTK -> Bool+ tkMatchesRecipientFingerprint rid tk =+ any+ (keyPktMatchesRecipientFingerprint rid)+ (_tkPrimaryKey tk : map fst (_tkSubs tk))++ keyPktMatchesRecipientFingerprint+ :: Fingerprint -> KeyPkt 'SecretPkt -> Bool+ keyPktMatchesRecipientFingerprint rid (KeyPktSecretPrimary pkp _) =+ pkPayloadMatchesRecipientFingerprint rid pkp+ keyPktMatchesRecipientFingerprint rid (KeyPktSecretSubkey pkp _) =+ pkPayloadMatchesRecipientFingerprint rid pkp++ pkPayloadMatchesRecipientFingerprint+ :: Fingerprint -> SomePKPayload -> Bool+ pkPayloadMatchesRecipientFingerprint rid pkp =+ fingerprint pkp `elem` recipientFingerprintMatchVariants rid++ recipientFingerprintMatchVariants :: Fingerprint -> [Fingerprint]+ recipientFingerprintMatchVariants (Fingerprint rid)+ | BL.length rid == 20 =+ [Fingerprint rid, Fingerprint (BL.cons 0x04 rid)]+ | BL.length rid == 21 && BL.head rid == 0x04 =+ [Fingerprint rid, Fingerprint (BL.tail rid)]+ | BL.length rid == 32 =+ [Fingerprint rid, Fingerprint (BL.cons 0x06 rid)]+ | BL.length rid == 33 && BL.head rid == 0x06 =+ [Fingerprint rid, Fingerprint (BL.tail rid)]+ | otherwise = [Fingerprint rid]++ keyringCandidates :: [TK 'SecretTK] -> IO [PKESKRecipientKey]+ keyringCandidates tks = concat <$> mapM tkCandidates tks++ tkCandidates :: TK 'SecretTK -> IO [PKESKRecipientKey]+ tkCandidates tk =+ fmap catMaybes . mapM resolveKeyPair $+ _tkPrimaryKey tk : map fst (_tkSubs tk)++ resolveKeyPair+ :: KeyPkt 'SecretPkt -> IO (Maybe PKESKRecipientKey)+ resolveKeyPair (KeyPktSecretPrimary pkp (SUUnencrypted sk _)) =+ pure $+ Just+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Just pkp+ , pkeskRecipientSKey = sk+ }+ resolveKeyPair (KeyPktSecretSubkey pkp (SUUnencrypted sk _)) =+ pure $+ Just+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Just pkp+ , pkeskRecipientSKey = sk+ }+ resolveKeyPair (KeyPktSecretPrimary pkp ska) = unlockProtected pkp ska+ resolveKeyPair (KeyPktSecretSubkey pkp ska) = unlockProtected pkp ska++ unlockProtected+ :: SomePKPayload -> SKAddendum -> IO (Maybe PKESKRecipientKey)+ unlockProtected pkp ska =+ case maybePassphraseCb of+ Nothing -> pure Nothing+ Just passphraseCb -> do+ mPassphrase <- passphraseCb pkp+ case mPassphrase of+ Nothing -> pure Nothing+ Just passphrase ->+ case decryptPrivateKey (pkp, ska) passphrase of+ Left _ -> pure Nothing+ Right (SUUnencrypted sk _) ->+ pure $+ Just+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Just pkp+ , pkeskRecipientSKey = sk+ }+ Right _ -> pure Nothing++buildUnwrapCandidatesResolver+ :: (KeyIdentifier -> PubKeyAlgorithm -> IO [PKESKRecipientKey])+ -> IO (PKESKResolver IO)+buildUnwrapCandidatesResolver cb = do+ stateRef <-+ newIORef+ ( Nothing :: Maybe PKESKPayload+ , [] :: [(Pkt, [PKESKRecipientKey])]+ , [] :: [SKey]+ )+ pure $ \req -> do+ let pkesk = reqPKESK req+ probePkt = reqProbePacket req+ (lastPKESK, probeState, seenSKeys) <- readIORef stateRef+ let freshPKESK = Just pkesk /= lastPKESK+ when freshPKESK $ writeIORef stateRef (Just pkesk, [], [])+ let state0 = if freshPKESK then [] else probeState+ seen0 = if freshPKESK then [] else seenSKeys+ case lookup probePkt state0 of+ Just (next : rest) -> do+ writeIORef+ stateRef+ ( Just pkesk+ , updateProbeState probePkt rest state0+ , pkeskRecipientSKey next : seen0+ )+ pure (ResolveWith next)+ Just [] ->+ pure ResolveSkip+ Nothing -> do+ candidates <-+ filterFreshCandidates seen0 <$> callbackCandidates probePkt+ case candidates of+ [] -> do+ writeIORef+ stateRef+ (Just pkesk, updateProbeState probePkt [] state0, seen0)+ pure ResolveSkip+ (next : rest) -> do+ writeIORef+ stateRef+ ( Just pkesk+ , updateProbeState probePkt rest state0+ , pkeskRecipientSKey next : seen0+ )+ pure (ResolveWith next)+ where+ callbackCandidates probePkt =+ cb+ (extractProbeKeyIdentifier probePkt)+ (extractProbePKA probePkt)++ updateProbeState probePkt remaining state0 =+ (probePkt, remaining) : filter ((/= probePkt) . fst) state0++ filterFreshCandidates seenSKeys =+ filter+ (\candidate -> pkeskRecipientSKey candidate `notElem` seenSKeys)++-- | Extract the recipient identifier from a PKESK probe packet.+extractProbeKeyIdentifier :: Pkt -> KeyIdentifier+extractProbeKeyIdentifier (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ rid _ _)))+ | isWildcardV3RecipientKeyId rid = KeyIdentifierWildcard+ | otherwise = KeyIdentifierEightOctet rid+extractProbeKeyIdentifier (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid _ _)))+ | BL.null rid = KeyIdentifierWildcard+ | otherwise = KeyIdentifierFingerprint (Fingerprint rid)+extractProbeKeyIdentifier _ = KeyIdentifierWildcard++isWildcardV3RecipientKeyId :: EightOctetKeyId -> Bool+isWildcardV3RecipientKeyId (EightOctetKeyId rid) =+ BL.length rid == 8 && BL.all (== 0) rid++-- | Extract the public-key algorithm from a PKESK probe packet.+extractProbePKA :: Pkt -> PubKeyAlgorithm+extractProbePKA (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ _ pka _))) = pka+extractProbePKA (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 _ pka _))) = pka+extractProbePKA _ = RSA -- fallback; should not be reached++-- | Core implementation: manual await loop so we can return a 'DecryptOutcome'.+conduitDecryptChecked'+ :: (MonadFail m, MonadResource m, MonadThrow m, MonadUnliftIO m)+ => RecursorState+ -> Bool+ -> PKESKResolver IO+ -> InputCallback IO+ -> Maybe (IORef [DecryptSessionKeyResolutionReport])+ -> ConduitT Pkt Pkt m DecryptOutcome+conduitDecryptChecked' rs0 allowManualPKESKPrompt pkcb cb reportRef =+ loop (SomeDecryptStreamState (ActiveDecryptState rs0))+ where+ loop+ :: (MonadFail m, MonadResource m, MonadThrow m, MonadUnliftIO m)+ => SomeDecryptStreamState -> ConduitT Pkt Pkt m DecryptOutcome+ loop streamState = do+ case streamState of+ SomeDecryptStreamState (MalformedDecryptState _ reason) ->+ return (DecryptMalformedStructure reason)+ SomeDecryptStreamState state -> do+ mpkt <- await+ case mpkt of+ Nothing -> return (finalOutcome state)+ Just pkt -> do+ (state', pkts) <- lift (push pkt state)+ mapM_ yield pkts+ loop state'++ push+ :: (MonadFail m, MonadResource m, MonadThrow m, MonadUnliftIO m)+ => Pkt+ -> DecryptStreamState phase+ -> m (SomeDecryptStreamState, [Pkt])+ push i (ActiveDecryptState s)+ | _depth s > 42 = fail "I think we've been quine-attacked"+ | hasPendingESKPrelude s && not (packetCanFollowESKPrelude i) =+ return+ ( SomeDecryptStreamState+ ( MalformedDecryptState+ s+ "Malformed encrypted packet sequence: ESK packets must immediately precede encrypted data"+ )+ , []+ )+ | otherwise =+ let dp = _decryptPolicy s+ in case i of+ SKESKPkt payload ->+ do+ when (decryptRejectDeprecatedSKESK dp) $+ case skeskPayloadS2K payload of+ Simple _ ->+ fail+ "SKESK uses Simple S2K specifier, which is deprecated by RFC9580 policy"+ Salted _ _ ->+ fail+ "SKESK uses Salted S2K specifier, which is deprecated by RFC9580 policy"+ _ -> pure ()+ return+ ( SomeDecryptStreamState+ ( ActiveDecryptState+ (s {_pendingESKs = PendingSKESK payload : _pendingESKs s})+ )+ , []+ )+ PKESKPkt p ->+ return+ ( SomeDecryptStreamState+ ( ActiveDecryptState+ (s {_pendingESKs = PendingPKESK p : _pendingESKs s})+ )+ , []+ )+ (SymEncDataPkt bs) ->+ if hasESKPayloadVersionMismatch dp LegacyEncryptedPayload s+ then+ return+ ( SomeDecryptStreamState+ ( MalformedDecryptState+ s+ ( "ESK/payload version mismatch: ESK packets present but none are version-aligned with "+ ++ "legacy SED payload"+ )+ )+ , []+ )+ else do+ when (not (decryptAllowSEDNoIntegrity dp)) $+ fail+ "Received unauthenticated SED (Symmetrically Encrypted Data) packet; \+ \RFC9580 policy requires integrity-protected SEIPD. \+ \Use lenientDecryptPolicy to permit legacy messages."+ (symalgo, sessionKey) <-+ resolveSessionKey+ s+ allowManualPKESKPrompt+ pkcb+ cb+ LegacyEncryptedPayload+ reportRef+ checkDecryptSymmetricAlgo dp symalgo+ d <-+ decryptSEDP+ s {_pendingESKs = []}+ allowManualPKESKPrompt+ pkcb+ cb+ reportRef+ symalgo+ sessionKey+ bs+ -- SED is the terminal outer-stream packet.+ return (finalizeOuterEncryptedPayload s d)+ (SymEncIntegrityProtectedDataPkt (SEIPD1 _ bs)) ->+ if hasESKPayloadVersionMismatch dp LegacyEncryptedPayload s+ then+ return+ ( SomeDecryptStreamState+ ( MalformedDecryptState+ s+ ( "ESK/payload version mismatch: ESK packets present but none are version-aligned with "+ ++ "SEIPDv1 payload"+ )+ )+ , []+ )+ else do+ when (not (decryptAllowSEIPDv1 dp)) $+ fail+ "Received SEIPDv1 packet; decrypt policy requires SEIPDv2 only."+ (symalgo, sessionKey) <-+ resolveSessionKey+ s+ allowManualPKESKPrompt+ pkcb+ cb+ LegacyEncryptedPayload+ reportRef+ checkDecryptSymmetricAlgo dp symalgo+ d <-+ decryptSEIPDP+ s {_pendingESKs = []}+ allowManualPKESKPrompt+ pkcb+ cb+ reportRef+ symalgo+ sessionKey+ bs+ -- The outer SEIPD1 packet is terminal; inner MDC is handled by+ -- the recursive conduit's own _seenMessageEnd tracking.+ return (finalizeOuterEncryptedPayload s d)+ (SymEncIntegrityProtectedDataPkt (SEIPD2 sa aa chunkSize salt bs)) ->+ if hasESKPayloadVersionMismatch dp (SEIPDv2EncryptedPayload sa aa) s+ then+ return+ ( SomeDecryptStreamState+ ( MalformedDecryptState+ s+ ( "ESK/payload version mismatch: ESK packets present but none are version-aligned with "+ ++ "SEIPDv2 payload"+ )+ )+ , []+ )+ else do+ checkDecryptSymmetricAlgo dp sa+ checkDecryptAEADAlgo dp aa+ (_, sessionKey) <-+ resolveSessionKey+ s+ allowManualPKESKPrompt+ pkcb+ cb+ (SEIPDv2EncryptedPayload sa aa)+ reportRef+ d <-+ decryptSEIPDv2P+ s {_pendingESKs = []}+ allowManualPKESKPrompt+ pkcb+ cb+ reportRef+ sa+ aa+ chunkSize+ salt+ sessionKey+ bs+ -- SEIPD2 final AEAD tag was verified inside decryptSEIPDv2P.+ return (finalizeOuterEncryptedPayload s d)+ m@(ModificationDetectionCodePkt mdc) -> do+ when (isNothing (_lastClearText s)) $ fail "MDC with no referent"+ let mcalculated = calculateMDC <$> _lastNonce s <*> _lastClearText s+ expectedMdc <-+ case mcalculated of+ Nothing -> fail "MDC with no nonce or cleartext"+ Just Nothing -> fail "MDC referent is too short"+ Just (Just x) -> return x+ when (expectedMdc /= mdc) $+ fail $+ "MDC indicates tampering: "+ ++ show mdc+ ++ " versus "+ ++ maybe "<empty>" show mcalculated+ ++ " ... "+ ++ show (_lastNonce s)+ ++ " / "+ ++ show (_lastClearText s)+ -- MDC is the integrity boundary inside a SEIPD1 inner stream.+ return+ ( SomeDecryptStreamState (FinishedDecryptState s False)+ , [m]+ )+ -- RFC9580 Padding Packet (tag 21) can be ignored after decryption.+ (PaddingPkt _) ->+ return (SomeDecryptStreamState (ActiveDecryptState s), [])+ (OtherPacketPkt t _)+ | t < 40 ->+ fail+ ("Unknown critical packet type in packet sequence: " ++ show t)+ (OtherPacketPkt _ _) ->+ return (SomeDecryptStreamState (ActiveDecryptState s), [])+ p ->+ return (SomeDecryptStreamState (ActiveDecryptState s), [p])+ push i (FinishedDecryptState s hadTrailing) =+ if decryptRejectTrailingData (_decryptPolicy s)+ then+ return+ ( SomeDecryptStreamState+ ( MalformedDecryptState+ s+ "packet received after message integrity boundary"+ )+ , []+ )+ else+ return+ (SomeDecryptStreamState (FinishedDecryptState s True), [i])+ push _ malformedState@(MalformedDecryptState _ _) =+ return (SomeDecryptStreamState malformedState, [])++ hasPendingESKPrelude s = not (null (_pendingESKs s))++ hasESKPayloadVersionMismatch dp payloadFlavor state =+ decryptRejectESKVersionMismatch dp+ && hasPendingESKPrelude state+ && null (alignedPrecedingESKs payloadFlavor (_pendingESKs state))++ finalizeOuterEncryptedPayload state decryptedPkts =+ ( SomeDecryptStreamState+ (FinishedDecryptState (state {_pendingESKs = []}) False)+ , decryptedPkts+ )++ finalOutcome :: DecryptStreamState phase -> DecryptOutcome+ finalOutcome (ActiveDecryptState _) = DecryptTruncated+ finalOutcome (FinishedDecryptState _ hadTrailing) =+ if hadTrailing+ then DecryptTrailingData+ else DecryptClean+ finalOutcome (MalformedDecryptState _ reason) =+ DecryptMalformedStructure reason++ packetCanFollowESKPrelude pkt =+ case pkt of+ SKESKPkt _ -> True+ PKESKPkt _ -> True+ SymEncDataPkt _ -> True+ SymEncIntegrityProtectedDataPkt _ -> True+ MarkerPkt _ -> True+ OtherPacketPkt 21 _ -> True+ _ -> False++{- | Describes what integrity-terminating marker (if any) an inner packet+stream is expected to contain. Passed to 'checkInnerOutcome' to+distinguish a legitimate end-of-stream from a missing marker.+-}+data InnerIntegrityExpectation+ = {- | The inner stream has no integrity-terminating packet. SED has none+ by design; SEIPD2 authenticates via AEAD before the conduit runs.+ 'DecryptTruncated' is the normal end-of-stream outcome.+ -}+ NoIntegrityMarker++{- | Propagate non-clean inner-stream outcomes as a 'fail'. Called after each+recursive decrypt helper so that structural violations and (for SEIPD1) a+missing MDC are not silently swallowed.+-}+checkInnerOutcome+ :: MonadFail m+ => InnerIntegrityExpectation -> DecryptOutcome -> m ()+checkInnerOutcome _ DecryptClean = pure ()+checkInnerOutcome _ DecryptTrailingData = pure ()+checkInnerOutcome NoIntegrityMarker DecryptTruncated = pure ()+checkInnerOutcome _ (DecryptMalformedStructure reason) =+ fail+ ("Inner encrypted payload had malformed structure: " ++ reason)++decryptSEDP+ :: (MonadFail m, MonadIO m, MonadThrow m, MonadUnliftIO m)+ => RecursorState+ -> Bool+ -> PKESKResolver IO+ -> InputCallback IO+ -> Maybe (IORef [DecryptSessionKeyResolutionReport])+ -> SymmetricAlgorithm+ -> SessionKey+ -> BL.ByteString+ -> m [Pkt]+decryptSEDP rs allowManualPKESKPrompt pkcb cb reportRef symalgo (SessionKey sessionKey) bs = do+ decrypted <-+ case decryptOpenPGPCfb symalgo (BL.toStrict bs) sessionKey of+ Left e -> fail (renderCipherError e)+ Right x -> pure x+ (innerOutcome, pkts) <-+ decryptInnerPackets+ rs+ allowManualPKESKPrompt+ pkcb+ cb+ reportRef+ decrypted+ checkInnerOutcome NoIntegrityMarker innerOutcome+ pure pkts++decryptSEIPDP+ :: (MonadFail m, MonadIO m, MonadThrow m, MonadUnliftIO m)+ => RecursorState+ -> Bool+ -> PKESKResolver IO+ -> InputCallback IO+ -> Maybe (IORef [DecryptSessionKeyResolutionReport])+ -> SymmetricAlgorithm+ -> SessionKey+ -> BL.ByteString+ -> m [Pkt]+decryptSEIPDP rs allowManualPKESKPrompt pkcb cb reportRef symalgo (SessionKey sessionKey) bs = do+ (nonce, decrypted) <-+ case decryptPreservingNonce symalgo (BL.toStrict bs) sessionKey of+ Left e -> fail (renderCipherError e)+ Right x -> pure x+ decryptedWithoutMDC <-+ case validateSEIPD1MDC nonce decrypted of+ Left err -> fail err+ Right x -> pure x+ (innerOutcome, pkts) <-+ decryptInnerPackets+ rs+ allowManualPKESKPrompt+ pkcb+ cb+ reportRef+ decryptedWithoutMDC+ checkInnerOutcome NoIntegrityMarker innerOutcome+ pure pkts++decryptSEIPDv2P+ :: (MonadFail m, MonadIO m, MonadThrow m, MonadUnliftIO m)+ => RecursorState+ -> Bool+ -> PKESKResolver IO+ -> InputCallback IO+ -> Maybe (IORef [DecryptSessionKeyResolutionReport])+ -> SymmetricAlgorithm+ -> AEADAlgorithm+ -> Word8+ -> Salt+ -> SessionKey+ -> BL.ByteString+ -> m [Pkt]+decryptSEIPDv2P rs allowManualPKESKPrompt pkcb cb reportRef symalgo aeadalgo chunkSize salt sessionKey bs = do+ let decrypted =+ decryptSEIPDv2Payload+ symalgo+ aeadalgo+ chunkSize+ salt+ (BL.toStrict bs)+ sessionKey+ case decrypted of+ Left e -> fail e+ Right cleartext -> do+ (innerOutcome, pkts) <-+ decryptInnerPackets+ rs+ allowManualPKESKPrompt+ pkcb+ cb+ reportRef+ cleartext+ checkInnerOutcome NoIntegrityMarker innerOutcome+ pure pkts++decryptInnerPackets+ :: (MonadFail m, MonadThrow m, MonadUnliftIO m)+ => RecursorState+ -> Bool+ -> PKESKResolver IO+ -> InputCallback IO+ -> Maybe (IORef [DecryptSessionKeyResolutionReport])+ -> B.ByteString+ -> m (DecryptOutcome, [Pkt])+decryptInnerPackets rs allowManualPKESKPrompt pkcb cb reportRef cleartext =+ runConduitRes $+ CB.sourceLbs (BL.fromStrict cleartext)+ .| conduitGet get+ .| conduitDecompress+ .| fuseBoth+ ( conduitDecryptChecked'+ rs {_depth = _depth rs + 1}+ allowManualPKESKPrompt+ pkcb+ cb+ reportRef+ )+ CL.consume++decryptSEIPDv2Payload+ :: SymmetricAlgorithm+ -> AEADAlgorithm+ -> Word8+ -> Salt+ -> B.ByteString+ -> SessionKey+ -> Either String B.ByteString+decryptSEIPDv2Payload symalgo aeadalgo chunkSize salt encrypted (SessionKey sessionKey) = do+ when (chunkSize > 16) $+ Left "SEIPD v2 chunk size octet must be between 0 and 16"+ (mode, nonceSize) <- aeadModeAndNonceSize aeadalgo+ keyLen <- symKeySize symalgo+ let outputLen = keyLen + nonceSize - 8+ when (B.length (unSalt salt) /= 32) $+ Left "SEIPD v2 salt must be exactly 32 octets"+ when (B.length encrypted < 32) $+ Left+ "SEIPD v2 ciphertext must include at least one chunk tag and a final tag"+ let info =+ B.pack+ [0xd2, 2, fromFVal symalgo, fromFVal aeadalgo, chunkSize]+ prk = extract @CHA.SHA256 (unSalt salt) sessionKey+ okm = expand @CHA.SHA256 prk info outputLen :: B.ByteString+ messageKey = B.take keyLen okm+ noncePrefix = B.take (nonceSize - 8) (B.drop keyLen okm)+ decryptSEIPDv2WithKey+ symalgo+ mode+ chunkSize+ info+ noncePrefix+ messageKey+ encrypted++decryptSEIPDv2WithKey+ :: SymmetricAlgorithm+ -> CCT.AEADMode+ -> Word8+ -> B.ByteString+ -> B.ByteString+ -> B.ByteString+ -> B.ByteString+ -> Either String B.ByteString+decryptSEIPDv2WithKey symalgo mode chunkSize info noncePrefix sessionKey encrypted =+ withAESCipher+ "SEIPD v2 decrypt currently supports AES-128/192/256 only"+ symalgo+ sessionKey+ (decryptChunks mode info chunkSize noncePrefix encrypted)++decryptChunks+ :: CCT.BlockCipher cipher+ => CCT.AEADMode+ -> B.ByteString+ -> Word8+ -> B.ByteString+ -> B.ByteString+ -> cipher+ -> Either String B.ByteString+decryptChunks 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+ decryptChunksWithReader = go 0 encrypted [] 0+ where+ chunkLen = 1 `shiftL` (fromIntegral chunkSize + 6)+ tagLen = 16++ go idx remaining acc totalPlain+ | B.length remaining < 2 * tagLen =+ lift $+ Left+ "SEIPD v2 ciphertext is too short for chunk and final authentication tags"+ | otherwise = do+ let hasMoreChunks = B.length remaining > chunkLen + 2 * tagLen+ currentChunkLen =+ if hasMoreChunks+ then chunkLen+ else B.length remaining - 2 * tagLen+ when (currentChunkLen < 0) $+ lift $+ Left "SEIPD v2 malformed chunk lengths"+ let (chunkCiphertext, r1) = B.splitAt currentChunkLen remaining+ (chunkTag, r2) = B.splitAt tagLen r1+ plainChunk <-+ decryptChunkWithContext idx chunkCiphertext chunkTag+ if hasMoreChunks+ then+ go+ (idx + 1)+ r2+ (plainChunk : acc)+ (totalPlain + B.length plainChunk)+ else do+ when (B.length r2 /= tagLen) $+ lift $+ Left "SEIPD v2 missing final authentication tag"+ verifyFinalTagWithContext+ (idx + 1)+ (totalPlain + B.length plainChunk)+ r2+ return (B.concat (reverse (plainChunk : acc)))++ decryptChunkWithContext idx chunkCiphertext chunkTag = do+ AEADDecryptContext mode' _ _ noncePrefix' cipher' <- ask+ if mode' == CCT.AEAD_OCB+ then+ lift $+ decryptWithOCBRFC7253With+ (\_ _ _ _ _ _ -> "SEIPD v2 chunk authentication failed")+ cipher'+ (noncePrefix' <> encodeWord64be idx)+ info+ chunkCiphertext+ (mkAuthTag chunkTag)+ else do+ aead <- initAEADWithContext idx+ let mPlain =+ CCT.aeadSimpleDecrypt+ aead+ info+ chunkCiphertext+ (mkAuthTag chunkTag)+ case mPlain of+ Nothing -> lift $ Left "SEIPD v2 chunk authentication failed"+ Just p -> return p++ verifyFinalTagWithContext idx totalPlain finalTag = do+ AEADDecryptContext mode' _ _ noncePrefix' cipher' <- ask+ if mode' == CCT.AEAD_OCB+ then do+ plain <-+ lift $+ decryptWithOCBRFC7253With+ (\_ _ _ _ _ _ -> "SEIPD v2 chunk authentication failed")+ cipher'+ (noncePrefix' <> encodeWord64be idx)+ (info <> encodeWord64be (fromIntegral totalPlain))+ B.empty+ (mkAuthTag finalTag)+ if B.null plain+ then return ()+ else+ lift $+ Left "SEIPD v2 final authentication tag verification failed"+ else do+ aead <- initAEADWithContext idx+ let mEmpty =+ CCT.aeadSimpleDecrypt+ aead+ (info <> encodeWord64be (fromIntegral totalPlain))+ B.empty+ (mkAuthTag finalTag)+ case mEmpty of+ Just p | B.null p -> return ()+ _ ->+ lift $+ Left "SEIPD v2 final authentication tag verification failed"++ initAEADWithContext idx = do+ AEADDecryptContext mode' _ _ noncePrefix' cipher' <- ask+ lift $+ first show . CE.eitherCryptoError $+ CCT.aeadInit mode' cipher' (noncePrefix' <> encodeWord64be idx)++aeadModeAndNonceSize+ :: AEADAlgorithm -> Either String (CCT.AEADMode, Int)+aeadModeAndNonceSize =+ aeadModeAndNonceSizeForSEIPDv2+ "Unknown AEAD algorithm for SEIPD v2 decrypt"++symKeySize :: SymmetricAlgorithm -> Either String Int+symKeySize =+ seipdv2SymmetricKeySize+ "SEIPD v2 decrypt currently supports AES-128/192/256 only"++encodeWord64be :: Word64 -> B.ByteString+encodeWord64be = BL.toStrict . runPut . putWord64be++mkAuthTag :: B.ByteString -> CCT.AuthTag+mkAuthTag = CCT.AuthTag . BA.convert++checkDecryptSymmetricAlgo+ :: MonadFail m => DecryptPolicy -> SymmetricAlgorithm -> m ()+checkDecryptSymmetricAlgo dp sa =+ case decryptAllowedSymmetricAlgos dp of+ Nothing -> pure ()+ Just allowed+ | sa `elem` allowed -> pure ()+ | otherwise ->+ fail $+ "Decrypt policy rejects symmetric algorithm "+ ++ show sa+ ++ "; allowed: "+ ++ show allowed++checkDecryptAEADAlgo+ :: MonadFail m => DecryptPolicy -> AEADAlgorithm -> m ()+checkDecryptAEADAlgo dp aa =+ case decryptAllowedAEADAlgos dp of+ Nothing -> pure ()+ Just allowed+ | aa `elem` allowed -> pure ()+ | otherwise ->+ fail $+ "Decrypt policy rejects AEAD algorithm "+ ++ show aa+ ++ "; allowed: "+ ++ show allowed++skeskPayloadSymmetricAlgorithm+ :: SKESKPayload -> SymmetricAlgorithm+skeskPayloadSymmetricAlgorithm payload =+ case classifySKESKPayload payload of+ ClassifiedSKESKPayloadV4 (SKESKPayloadV4 sa _ _) -> sa+ ClassifiedSKESKPayloadV6 (SKESKPayloadV6 sa _ _ _ _ _) -> sa++skeskPayloadS2K :: SKESKPayload -> S2K+skeskPayloadS2K payload =+ case classifySKESKPayload payload of+ ClassifiedSKESKPayloadV4 (SKESKPayloadV4 _ s2k _) -> s2k+ ClassifiedSKESKPayloadV6 (SKESKPayloadV6 _ _ s2k _ _ _) -> s2k++skeskPayloadAEADAlgorithm :: SKESKPayload -> Maybe AEADAlgorithm+skeskPayloadAEADAlgorithm payload =+ case classifySKESKPayload payload of+ ClassifiedSKESKPayloadV4 _ -> Nothing+ ClassifiedSKESKPayloadV6 (SKESKPayloadV6 _ aa _ _ _ _) -> Just aa++resolveSKESKSessionKey+ :: BL.ByteString -> SKESKPayload -> Either String B.ByteString+resolveSKESKSessionKey passphrase payload =+ first renderSKESKSessionKeyResolutionError $+ resolveSKESKSessionKeyTyped+ passphrase+ (classifySKESKPayload payload)++data SKESKSessionKeyResolutionError+ = SKESKSessionKeyS2KError S2KError+ | SKESKSessionKeyOtherError String+ deriving (Eq, Show)++renderSKESKSessionKeyResolutionError+ :: SKESKSessionKeyResolutionError -> String+renderSKESKSessionKeyResolutionError (SKESKSessionKeyS2KError err) = renderS2KError err+renderSKESKSessionKeyResolutionError (SKESKSessionKeyOtherError err) = err++resolveSKESKSessionKeyTyped+ :: BL.ByteString+ -> ClassifiedSKESKPayload+ -> Either SKESKSessionKeyResolutionError B.ByteString+resolveSKESKSessionKeyTyped passphrase (ClassifiedSKESKPayloadV4 (SKESKPayloadV4 sa s2k Nothing)) =+ first+ SKESKSessionKeyS2KError+ (skesk2Key (SKESK4Packet sa s2k Nothing) passphrase)+resolveSKESKSessionKeyTyped passphrase (ClassifiedSKESKPayloadV4 (SKESKPayloadV4 sa s2k (Just esk))) =+ first+ SKESKSessionKeyS2KError+ ( snd+ <$> skesk2SessionKey (SKESK4Packet sa s2k (Just esk)) passphrase+ )+resolveSKESKSessionKeyTyped passphrase (ClassifiedSKESKPayloadV6 (SKESKPayloadV6 sa aead s2k iv esk tag)) = do+ keyLen <-+ first+ (SKESKSessionKeyS2KError . S2KUnsupportedAlgorithm)+ (keySize sa)+ ikm <-+ first SKESKSessionKeyS2KError (string2Key s2k keyLen passphrase)+ kek <-+ first SKESKSessionKeyOtherError (deriveSKESK6KEK sa aead ikm)+ first+ SKESKSessionKeyOtherError+ ( decryptSKESK6SessionKey+ sa+ aead+ kek+ (BL.toStrict iv)+ (BL.toStrict esk)+ (BL.toStrict tag)+ )++data ClassifiedSKESKPayload where+ ClassifiedSKESKPayloadV4+ :: SKESKPayloadV4 -> ClassifiedSKESKPayload+ ClassifiedSKESKPayloadV6+ :: SKESKPayloadV6 -> ClassifiedSKESKPayload++classifySKESKPayload :: SKESKPayload -> ClassifiedSKESKPayload+classifySKESKPayload (SKESKPayloadV4Packet payloadV4) =+ ClassifiedSKESKPayloadV4 payloadV4+classifySKESKPayload (SKESKPayloadV6Packet payloadV6) =+ ClassifiedSKESKPayloadV6 payloadV6++resolveSessionKey+ :: (MonadFail m, MonadIO m)+ => RecursorState+ -> Bool+ -> PKESKResolver IO+ -> InputCallback IO+ -> EncryptedPayloadFlavor v+ -> Maybe (IORef [DecryptSessionKeyResolutionReport])+ -> m (SymmetricAlgorithm, SessionKey)+resolveSessionKey rs allowManualPKESKPrompt pkcb cb payloadFlavor reportRef =+ case precedingCandidates of+ [] ->+ if null (_pendingESKs rs)+ then+ fail+ "Encrypted data packet has no preceding SKESK or PKESK packet"+ else+ fail+ "Encrypted data packet has no preceding SKESK or PKESK packet aligned with payload version"+ candidates ->+ case skeskCandidates candidates of+ [] -> resolvePKESKCandidates (pkeskCandidates candidates) [] [] []+ skesks -> do+ passphrase <- liftIO $ cb "Input the passphrase I want"+ resolveSKESKCandidates+ passphrase+ skesks+ (pkeskCandidates candidates)+ []+ where+ precedingCandidates+ | strictAlignment = alignedByVersion+ | null alignedByVersion = _pendingESKs rs+ | otherwise = alignedByVersion++ strictAlignment = decryptRejectESKVersionMismatch (_decryptPolicy rs)+ alignedByVersion = pendingESKsFromAligned alignedStrict+ alignedStrict = alignedPrecedingESKs payloadFlavor (_pendingESKs rs)++ skeskCandidates esks = [skesk | PendingSKESK skesk <- esks]++ pkeskCandidates esks = [pkesk | PendingPKESK pkesk <- esks]++ resolveSKESKCandidates+ :: (MonadFail m, MonadIO m)+ => BL.ByteString+ -> [SKESKPayload]+ -> [PKESKPayload]+ -> [String]+ -> m (SymmetricAlgorithm, SessionKey)+ resolveSKESKCandidates _ [] pkesks skeskErrs =+ resolvePKESKCandidates pkesks skeskErrs [] []+ resolveSKESKCandidates passphrase (skesk : rest) pkesks skeskErrs =+ case resolveSKESKCandidate passphrase skesk of+ Left err ->+ resolveSKESKCandidates+ passphrase+ rest+ pkesks+ ((skeskErrPrefix skesk ++ err) : skeskErrs)+ Right resolved -> do+ emitResolutionReport+ (mkResolutionReport DecryptResolvedViaSKESK skeskErrs [] [])+ pure resolved++ resolveSKESKCandidate+ :: BL.ByteString+ -> SKESKPayload+ -> Either String (SymmetricAlgorithm, SessionKey)+ resolveSKESKCandidate passphrase skesk = do+ let skeskSymAlgo = skeskPayloadSymmetricAlgorithm skesk+ expectedSymAlgo = payloadExpectedSymmetricAlgorithm payloadFlavor+ sessionKeyBytes <- resolveSKESKSessionKey passphrase skesk+ case expectedSymAlgo of+ Just expected+ | expected /= skeskSymAlgo ->+ Left "SKESK/encrypted-payload symmetric algorithm mismatch"+ _ ->+ case ( payloadExpectedAEADAlgorithm payloadFlavor+ , skeskPayloadAEADAlgorithm skesk+ ) of+ (Just expectedAEAD, Just skeskAEAD)+ | expectedAEAD /= skeskAEAD ->+ Left "SKESK/encrypted-payload AEAD algorithm mismatch"+ _ -> Right (skeskSymAlgo, SessionKey sessionKeyBytes)++ skeskErrPrefix skesk = "[" ++ describeSKESK skesk ++ "] "++ resolvePKESKCandidates+ :: (MonadFail m, MonadIO m)+ => [PKESKPayload]+ -> [String]+ -> [String]+ -> [PKESKResolverAttempt]+ -> m (SymmetricAlgorithm, SessionKey)+ resolvePKESKCandidates [] [] [] _ =+ fail+ "Encrypted data packet has no usable preceding SKESK or PKESK packet"+ resolvePKESKCandidates [] skeskErrs [] _ =+ fail+ ( "Encrypted data packet has no usable preceding SKESK or PKESK packet; "+ ++ "candidate errors: "+ ++ unwords (reverse skeskErrs)+ )+ resolvePKESKCandidates [] skeskErrs pkeskErrs resolverAttempts =+ if allowManualPKESKPrompt+ then do+ let expectedSymAlgo = payloadExpectedSymmetricAlgorithm payloadFlavor+ errs = skeskErrs ++ pkeskErrs+ encodedSessionKey <-+ BL.toStrict+ <$> liftIO+ ( cb+ "Input decrypted PKESK session key material (OpenPGP encoded or raw key bytes)"+ )+ case decodePKESKSessionKey expectedSymAlgo encodedSessionKey of+ Left manualErr ->+ fail+ ( "Encrypted data packet has no usable preceding SKESK or PKESK packet; "+ ++ "candidate errors: "+ ++ unwords (reverse errs)+ ++ "; manual input failed: "+ ++ manualErr+ )+ Right (sa, k) -> do+ emitResolutionReport+ ( mkResolutionReport+ DecryptResolvedViaManualPKESKInput+ skeskErrs+ pkeskErrs+ resolverAttempts+ )+ pure (sa, SessionKey k)+ else+ fail+ ( "Encrypted data packet has no usable preceding SKESK or PKESK packet; "+ ++ "candidate errors: "+ ++ unwords (reverse (skeskErrs ++ pkeskErrs))+ )+ resolvePKESKCandidates (pkesk : rest) skeskErrs pkeskErrs resolverAttempts = do+ let expectedSymAlgo = payloadExpectedSymmetricAlgorithm payloadFlavor+ errPrefix = "[" ++ describePKESK pkesk ++ "] "+ attemptPKESKCandidate+ []+ []+ 0+ expectedSymAlgo+ errPrefix+ resolverAttempts+ where+ attemptPKESKCandidate attemptedSKeys previousFailures attemptIndex expectedSymAlgo errPrefix resolverAttemptsAcc = do+ let callbackProbeSummary = describeCallbackProbeSummary pkesk+ resolverResult <-+ liftIO+ (resolvePKESKRecipientKey pkesk previousFailures attemptIndex)+ case resolverResult of+ Left resolverErr ->+ fail (errPrefix ++ resolverErr)+ Right (Nothing, _, newAttempts) ->+ let resolverAttempts' = resolverAttemptsAcc ++ newAttempts+ terminalError =+ case reverse previousFailures of+ (latestFailure : _) -> errPrefix ++ pkeskAttemptFailureReason latestFailure+ [] ->+ errPrefix+ ++ "no matching key context (callback probes: "+ ++ callbackProbeSummary+ ++ ")"+ in resolvePKESKCandidates+ rest+ skeskErrs+ (terminalError : pkeskErrs)+ resolverAttempts'+ Right (Just keyInfo, nextAttemptIndex, newAttempts)+ | pkeskRecipientSKey keyInfo `elem` attemptedSKeys ->+ let resolverAttempts' = resolverAttemptsAcc ++ newAttempts+ terminalError =+ case reverse previousFailures of+ (latestFailure : _) -> errPrefix ++ pkeskAttemptFailureReason latestFailure+ [] ->+ errPrefix+ ++ "key context callback repeated without yielding a usable key"+ in resolvePKESKCandidates+ rest+ skeskErrs+ (terminalError : pkeskErrs)+ resolverAttempts'+ | otherwise -> do+ let resolverAttempts' = resolverAttemptsAcc ++ newAttempts+ unwrapped <- liftIO (tryUnwrapPKESKSessionMaterial pkesk keyInfo)+ case unwrapped of+ Left err ->+ attemptPKESKCandidate+ (pkeskRecipientSKey keyInfo : attemptedSKeys)+ ( previousFailures+ ++ [mkAttemptFailure keyInfo PKESKAttemptUnwrapFailed err]+ )+ nextAttemptIndex+ expectedSymAlgo+ errPrefix+ resolverAttempts'+ Right encodedSessionKey ->+ case decodePKESKSessionKey expectedSymAlgo encodedSessionKey of+ Left err ->+ attemptPKESKCandidate+ (pkeskRecipientSKey keyInfo : attemptedSKeys)+ ( previousFailures+ ++ [ mkAttemptFailure+ keyInfo+ PKESKAttemptSessionMaterialDecodeFailed+ err+ ]+ )+ nextAttemptIndex+ expectedSymAlgo+ errPrefix+ resolverAttempts'+ Right (sa, k) -> do+ emitResolutionReport+ ( mkResolutionReport+ DecryptResolvedViaPKESK+ skeskErrs+ pkeskErrs+ resolverAttempts'+ )+ pure (sa, SessionKey k)++ emitResolutionReport+ :: MonadIO m => DecryptSessionKeyResolutionReport -> m ()+ emitResolutionReport report =+ case reportRef of+ Nothing -> pure ()+ Just ref -> liftIO (modifyIORef' ref (report :))++ mkResolutionReport+ :: DecryptSessionKeyResolutionPath+ -> [String]+ -> [String]+ -> [PKESKResolverAttempt]+ -> DecryptSessionKeyResolutionReport+ mkResolutionReport path skeskErrs pkeskErrs resolverAttempts =+ DecryptSessionKeyResolutionReport+ { decryptSessionResolutionPath = path+ , decryptSessionResolutionSKESKErrors = reverse skeskErrs+ , decryptSessionResolutionPKESKErrors = reverse pkeskErrs+ , decryptSessionResolutionResolverAttempts = resolverAttempts+ }++ mkAttemptFailure keyInfo failureKind reason =+ PKESKAttemptFailure+ { pkeskAttemptFailureKeyContext =+ recipientKeyContext keyInfo+ , pkeskAttemptFailureKind = failureKind+ , pkeskAttemptFailureReason = reason+ }++ recipientKeyContext+ :: PKESKRecipientKey -> Maybe (KeyVersion, PubKeyAlgorithm)+ recipientKeyContext keyInfo =+ fmap+ (\pk -> (_keyVersion pk, _pkalgo pk))+ (pkeskRecipientPKPayload keyInfo)++ renderPKESKResolverError (ResolverPolicyDenied reason) =+ "resolver policy denied candidate selection: " ++ reason+ renderPKESKResolverError (ResolverBackendUnavailable reason) =+ "resolver backend unavailable: " ++ reason+ renderPKESKResolverError (ResolverInvalidResponse reason) =+ "resolver returned an invalid response: " ++ reason++ resolvePKESKRecipientKey payload previousFailures attemptIndex0 =+ probePacketVariants+ attemptIndex0+ []+ (pkeskCallbackPackets payload)+ where+ probePacketVariants attemptIndex attemptsAcc [] =+ pure (Right (Nothing, attemptIndex, reverse attemptsAcc))+ probePacketVariants attemptIndex attemptsAcc (probePkt : restProbePkts) = do+ let request =+ PKESKResolveRequest+ { reqPKESK = payload+ , reqProbePacket = probePkt+ , reqIsWildcardRecipient = isWildcardPKESKPayload payload+ , reqAttemptIndex = attemptIndex+ , reqPreviousFailures = previousFailures+ }+ resolveAction <-+ pkcb request+ let attemptRecord =+ PKESKResolverAttempt+ { pkeskResolverAttemptPreviousFailures = previousFailures+ , pkeskResolverAttemptAction =+ case resolveAction of+ ResolveWith keyInfo ->+ ResolverAttemptResolveWith (recipientKeyContext keyInfo)+ ResolveSkip -> ResolverAttemptSkip+ ResolveExhausted -> ResolverAttemptExhausted+ ResolveFail resolverErr -> ResolverAttemptFail resolverErr+ }+ case resolveAction of+ ResolveWith keyInfo ->+ pure+ ( Right+ ( Just keyInfo+ , attemptIndex + 1+ , reverse (attemptRecord : attemptsAcc)+ )+ )+ ResolveSkip ->+ probePacketVariants+ (attemptIndex + 1)+ (attemptRecord : attemptsAcc)+ restProbePkts+ ResolveExhausted ->+ pure+ ( Right+ ( Nothing+ , attemptIndex + 1+ , reverse (attemptRecord : attemptsAcc)+ )+ )+ ResolveFail resolverErr ->+ pure (Left (renderPKESKResolverError resolverErr))++ pkeskCallbackPackets payload =+ nub $+ case payload of+ PKESKPayloadV3Packet (PKESKPayloadV3 v rid pka mpis) ->+ map+ ( \ridVariant ->+ PKESKPkt+ (PKESKPayloadV3Packet (PKESKPayloadV3 v ridVariant pka mpis))+ )+ (recipientIdCallbackVariantsV3 rid)+ PKESKPayloadV6Packet (PKESKPayloadV6 rid pka esk) ->+ map+ ( \ridVariant ->+ PKESKPkt+ (PKESKPayloadV6Packet (PKESKPayloadV6 ridVariant pka esk))+ )+ (recipientIdCallbackVariants rid)++ describeCallbackProbeSummary payload =+ intercalate+ ", "+ (map describePKESKCallbackProbe (pkeskCallbackPackets payload))++ describePKESKCallbackProbe (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ rid pka _))) =+ "PKESK3 "+ ++ show pka+ ++ " rid="+ ++ show rid+ ++ if isWildcardV3RecipientKeyId rid+ then " (wildcard)"+ else ""+ describePKESKCallbackProbe (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka _))) =+ "PKESK6 " ++ show pka ++ " rid=" ++ show rid+ describePKESKCallbackProbe pkt = show pkt++ recipientIdCallbackVariantsV3 rid+ | isWildcardV3RecipientKeyId rid = [rid]+ | otherwise = [rid, EightOctetKeyId (BL.replicate 8 0)]++ recipientIdCallbackVariants rid+ | BL.length rid == 20 = [rid, BL.cons 0x04 rid]+ | BL.length rid == 21 && BL.head rid == 0x04 = [rid, BL.tail rid]+ | BL.length rid == 32 = [rid, BL.cons 0x06 rid]+ | BL.length rid == 33 && BL.head rid == 0x06 = [rid, BL.tail rid]+ | otherwise = [rid]++ isWildcardPKESKPayload (PKESKPayloadV3Packet (PKESKPayloadV3 _ (EightOctetKeyId rid) _ _)) =+ isWildcardV3RecipientKeyId (EightOctetKeyId rid)+ isWildcardPKESKPayload _ = False+ describePKESK payload =+ case classifyPKESKPayload payload of+ ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ rid pka _) ->+ "PKESK3 " ++ show pka ++ " rid=" ++ show rid+ ClassifiedPKESKPayloadV6 (PKESKPayloadV6 rid pka _) ->+ "PKESK6 " ++ show pka ++ " rid=" ++ show rid++ describeSKESK payload =+ case classifySKESKPayload payload of+ ClassifiedSKESKPayloadV4 (SKESKPayloadV4 sa s2k _) ->+ "SKESK4 " ++ show sa ++ " s2k=" ++ show s2k+ ClassifiedSKESKPayloadV6 (SKESKPayloadV6 sa aa s2k _ _ _) ->+ "SKESK6 " ++ show sa ++ "/" ++ show aa ++ " s2k=" ++ show s2k++data AlignedPendingESK (v :: EncryptedPayloadVersion) where+ LegacyAlignedSKESK+ :: SKESKPayloadV4+ -> AlignedPendingESK 'LegacyEncryptedPayloadVersion+ LegacyAlignedPKESK+ :: PKESKPayloadV3+ -> AlignedPendingESK 'LegacyEncryptedPayloadVersion+ SEIPDv2AlignedSKESK+ :: SKESKPayloadV6+ -> AlignedPendingESK 'SEIPDv2EncryptedPayloadVersion+ SEIPDv2AlignedPKESK+ :: PKESKPayloadV6+ -> AlignedPendingESK 'SEIPDv2EncryptedPayloadVersion+ {- | A version 3 PKESK that precedes a SEIPDv2 payload.++ RFC 9580 §5.13 explicitly permits version 3 PKESKs before a version 2+ SEIPD packet, as a backward-compatibility allowance for implementations+ that cannot yet produce v6 key material. The v3 PKESK carries the+ session key wrapped with legacy (v3) asymmetric key wrapping; the SEIPD+ v2 payload itself is still authenticated with modern AEAD. This+ constructor therefore counts as "aligned" for SEIPDv2 — it does not+ indicate a mis-assembled message — even though the PKESK version does+ not match the SEIPD version.+ -}+ SEIPDv2AlignedLegacyPKESK+ :: PKESKPayloadV3+ -> AlignedPendingESK 'SEIPDv2EncryptedPayloadVersion++alignedPrecedingESKs+ :: EncryptedPayloadFlavor v+ -> [PendingESK]+ -> [AlignedPendingESK v]+alignedPrecedingESKs LegacyEncryptedPayload =+ mapMaybe+ ( \esk ->+ case esk of+ PendingSKESK (SKESKPayloadV4Packet skesk4) -> Just (LegacyAlignedSKESK skesk4)+ PendingPKESK (PKESKPayloadV3Packet pkesk3) -> Just (LegacyAlignedPKESK pkesk3)+ _ -> Nothing+ )+alignedPrecedingESKs (SEIPDv2EncryptedPayload _ _) =+ mapMaybe+ ( \esk ->+ case esk of+ PendingSKESK (SKESKPayloadV6Packet skesk6) -> Just (SEIPDv2AlignedSKESK skesk6)+ PendingPKESK (PKESKPayloadV6Packet pkesk6) -> Just (SEIPDv2AlignedPKESK pkesk6)+ -- v3 PKESK before SEIPDv2 is explicitly allowed by RFC 9580 §5.13;+ -- see 'SEIPDv2AlignedLegacyPKESK' for details.+ PendingPKESK (PKESKPayloadV3Packet pkesk3) -> Just (SEIPDv2AlignedLegacyPKESK pkesk3)+ _ -> Nothing+ )++pendingESKsFromAligned :: [AlignedPendingESK v] -> [PendingESK]+pendingESKsFromAligned =+ map+ ( \esk ->+ case esk of+ LegacyAlignedSKESK skesk4 -> PendingSKESK (SKESKPayloadV4Packet skesk4)+ LegacyAlignedPKESK pkesk3 -> PendingPKESK (PKESKPayloadV3Packet pkesk3)+ SEIPDv2AlignedSKESK skesk6 -> PendingSKESK (SKESKPayloadV6Packet skesk6)+ SEIPDv2AlignedPKESK pkesk6 -> PendingPKESK (PKESKPayloadV6Packet pkesk6)+ SEIPDv2AlignedLegacyPKESK pkesk3 -> PendingPKESK (PKESKPayloadV3Packet pkesk3)+ )++payloadExpectedSymmetricAlgorithm+ :: EncryptedPayloadFlavor v -> Maybe SymmetricAlgorithm+payloadExpectedSymmetricAlgorithm LegacyEncryptedPayload = Nothing+payloadExpectedSymmetricAlgorithm (SEIPDv2EncryptedPayload sa _) = Just sa++payloadExpectedAEADAlgorithm+ :: EncryptedPayloadFlavor v -> Maybe AEADAlgorithm+payloadExpectedAEADAlgorithm LegacyEncryptedPayload = Nothing+payloadExpectedAEADAlgorithm (SEIPDv2EncryptedPayload _ aa) = Just aa++tryUnwrapPKESKSessionMaterial+ :: PKESKPayload+ -> PKESKRecipientKey+ -> IO (Either String B.ByteString)+tryUnwrapPKESKSessionMaterial pkesk keyInfo = do+ attempted <-+ try @SomeException+ (unwrapPKESKSessionMaterial pkesk keyInfo :: IO B.ByteString)+ pure (first displayException attempted)++data PKESKUnwrapCase where+ PKESKUnwrapV3RSA :: RSATypes.PrivateKey -> MPI -> PKESKUnwrapCase+ PKESKUnwrapV6RSA+ :: RSATypes.PrivateKey -> B.ByteString -> PKESKUnwrapCase+ PKESKUnwrapV6ECDH+ :: PKESKRecipientKey+ -> PubKeyAlgorithm+ -> B.ByteString+ -> ECDSA.PrivateKey+ -> PKESKUnwrapCase+ PKESKUnwrapV6XDHRaw+ :: PKESKRecipientKey+ -> PubKeyAlgorithm+ -> B.ByteString+ -> B.ByteString+ -> PKESKUnwrapCase+ PKESKUnwrapV3X25519FromECDH+ :: PKESKRecipientKey+ -> NonEmpty MPI+ -> ECDSA.PrivateKey+ -> PKESKUnwrapCase+ PKESKUnwrapV3X25519Raw+ :: PKESKRecipientKey+ -> NonEmpty MPI+ -> B.ByteString+ -> PKESKUnwrapCase+ PKESKUnwrapV3ECDH+ :: PKESKRecipientKey+ -> PubKeyAlgorithm+ -> NonEmpty MPI+ -> ECDSA.PrivateKey+ -> PKESKUnwrapCase++data ClassifiedPKESKPayload where+ ClassifiedPKESKPayloadV3+ :: PKESKPayloadV3 -> ClassifiedPKESKPayload+ ClassifiedPKESKPayloadV6+ :: PKESKPayloadV6 -> ClassifiedPKESKPayload++data ClassifiedPKESKRecipientKey where+ ClassifiedPKESKRecipientRSA+ :: PKESKRecipientKey+ -> RSATypes.PrivateKey+ -> ClassifiedPKESKRecipientKey+ ClassifiedPKESKRecipientECDH+ :: PKESKRecipientKey+ -> ECDSA.PrivateKey+ -> ClassifiedPKESKRecipientKey+ ClassifiedPKESKRecipientX25519+ :: PKESKRecipientKey -> B.ByteString -> ClassifiedPKESKRecipientKey+ ClassifiedPKESKRecipientX448+ :: PKESKRecipientKey -> B.ByteString -> ClassifiedPKESKRecipientKey+ ClassifiedPKESKRecipientUnsupported+ :: PKESKRecipientKey -> ClassifiedPKESKRecipientKey++classifyPKESKPayload :: PKESKPayload -> ClassifiedPKESKPayload+classifyPKESKPayload (PKESKPayloadV3Packet payloadV3) =+ ClassifiedPKESKPayloadV3 payloadV3+classifyPKESKPayload (PKESKPayloadV6Packet payloadV6) =+ ClassifiedPKESKPayloadV6 payloadV6++classifyPKESKRecipientKey+ :: PKESKRecipientKey -> ClassifiedPKESKRecipientKey+classifyPKESKRecipientKey keyInfo@(PKESKRecipientKey _ (RSAPrivateKey (RSA_PrivateKey privateKey))) =+ ClassifiedPKESKRecipientRSA keyInfo privateKey+classifyPKESKRecipientKey+ keyInfo@( PKESKRecipientKey+ _+ (ECDHPrivateKey (ECDSA_PrivateKey privateKey))+ ) =+ ClassifiedPKESKRecipientECDH keyInfo privateKey+classifyPKESKRecipientKey keyInfo@(PKESKRecipientKey _ (X25519PrivateKey privateKeyRaw)) =+ ClassifiedPKESKRecipientX25519 keyInfo privateKeyRaw+classifyPKESKRecipientKey keyInfo@(PKESKRecipientKey _ (X448PrivateKey privateKeyRaw)) =+ ClassifiedPKESKRecipientX448 keyInfo privateKeyRaw+classifyPKESKRecipientKey keyInfo =+ ClassifiedPKESKRecipientUnsupported keyInfo++pkeskPayloadAlgorithm :: PKESKPayload -> PubKeyAlgorithm+pkeskPayloadAlgorithm payload =+ case classifyPKESKPayload payload of+ ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ _ pka _) -> pka+ ClassifiedPKESKPayloadV6 (PKESKPayloadV6 _ pka _) -> pka++classifyPKESKUnwrapCase+ :: PKESKPayload+ -> PKESKRecipientKey+ -> Either String PKESKUnwrapCase+classifyPKESKUnwrapCase payload keyInfo =+ case (classifyPKESKPayload payload, classifyPKESKRecipientKey keyInfo) of+ ( ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ _ pka (mpi :| []))+ , ClassifiedPKESKRecipientRSA _ privateKey+ )+ | pka == RSA || pka == DeprecatedRSAEncryptOnly ->+ Right (PKESKUnwrapV3RSA privateKey mpi)+ ( ClassifiedPKESKPayloadV6 (PKESKPayloadV6 _ pka esk)+ , ClassifiedPKESKRecipientRSA _ privateKey+ )+ | pka == RSA ->+ Right (PKESKUnwrapV6RSA privateKey (BL.toStrict esk))+ ( ClassifiedPKESKPayloadV6 (PKESKPayloadV6 _ pka esk)+ , ClassifiedPKESKRecipientECDH recipientCtx privateKey+ )+ | pka == ECDH || pka == X25519 ->+ Right+ (PKESKUnwrapV6ECDH recipientCtx pka (BL.toStrict esk) privateKey)+ ( ClassifiedPKESKPayloadV6 (PKESKPayloadV6 _ pka esk)+ , ClassifiedPKESKRecipientX25519 recipientCtx privateKeyRaw+ )+ | pka == X25519 ->+ Right+ ( PKESKUnwrapV6XDHRaw+ recipientCtx+ pka+ (BL.toStrict esk)+ privateKeyRaw+ )+ ( ClassifiedPKESKPayloadV6 (PKESKPayloadV6 _ pka esk)+ , ClassifiedPKESKRecipientX448 recipientCtx privateKeyRaw+ )+ | pka == X448 ->+ Right+ ( PKESKUnwrapV6XDHRaw+ recipientCtx+ pka+ (BL.toStrict esk)+ privateKeyRaw+ )+ ( ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ _ pka mpis)+ , ClassifiedPKESKRecipientECDH recipientCtx privateKey+ )+ | pka == X25519 ->+ Right (PKESKUnwrapV3X25519FromECDH recipientCtx mpis privateKey)+ ( ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ _ pka mpis)+ , ClassifiedPKESKRecipientX25519 recipientCtx privateKeyRaw+ )+ | pka == X25519 ->+ Right (PKESKUnwrapV3X25519Raw recipientCtx mpis privateKeyRaw)+ ( ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ _ pka mpis)+ , ClassifiedPKESKRecipientECDH recipientCtx privateKey+ )+ | pka == ECDH || pka == X25519 ->+ Right (PKESKUnwrapV3ECDH recipientCtx pka mpis privateKey)+ (ClassifiedPKESKPayloadV3 (PKESKPayloadV3 _ _ pka _), _) ->+ Left+ ("PKESK key unwrap unsupported for packet algorithm " ++ show pka)+ (ClassifiedPKESKPayloadV6 (PKESKPayloadV6 _ pka _), _) ->+ Left+ ( "PKESKv6 key unwrap unsupported for packet algorithm "+ ++ show pka+ ++ " with secret key "+ ++ show (pkeskRecipientSKey keyInfo)+ )++unwrapPKESKSessionMaterial+ :: (MonadFail m, MonadIO m)+ => PKESKPayload -> PKESKRecipientKey -> m B.ByteString+unwrapPKESKSessionMaterial pkesk keyInfo = do+ either fail pure (validateTable30Policy pkesk keyInfo)+ unwrapCase <-+ either fail pure (classifyPKESKUnwrapCase pkesk keyInfo)+ case unwrapCase of+ PKESKUnwrapV3RSA privateKey mpi ->+ rsaUnwrap+ privateKey+ (leftPadTo (rsaModulusBytes privateKey) (i2osp (unMPI mpi)))+ PKESKUnwrapV6RSA privateKey esk -> do+ normalized <-+ either fail pure (normalizePKESKv6RSAEsk privateKey esk)+ rsaUnwrap privateKey normalized+ PKESKUnwrapV6ECDH recipientCtx pka esk privateKey ->+ ecdhUnwrapV6 recipientCtx pka esk privateKey+ PKESKUnwrapV6XDHRaw recipientCtx pka esk privateKeyRaw ->+ ecdhUnwrapV6XDHRaw recipientCtx pka esk privateKeyRaw+ PKESKUnwrapV3X25519FromECDH recipientCtx mpis privateKey -> do+ recipientPKP <-+ maybe+ ( fail+ "X25519 PKESKv3 unwrap requires recipient PKPayload context; use conduitDecrypt with DecryptWithKeyring or DecryptWithUnwrapCandidatesCallback"+ )+ pure+ (pkeskRecipientPKPayload recipientCtx)+ recipientSecretRaw <-+ either fail pure (resolveX25519SecretRaw recipientPKP privateKey)+ x25519UnwrapV3 recipientCtx mpis recipientSecretRaw+ PKESKUnwrapV3X25519Raw recipientCtx mpis privateKeyRaw ->+ x25519UnwrapV3 recipientCtx mpis privateKeyRaw+ PKESKUnwrapV3ECDH recipientCtx pka mpis privateKey ->+ ecdhUnwrap recipientCtx pka mpis privateKey+ where+ validateTable30Policy+ :: PKESKPayload -> PKESKRecipientKey -> Either String ()+ validateTable30Policy payload recipientCtx =+ case pkeskRecipientPKPayload recipientCtx of+ Nothing -> Right ()+ Just recipientPKP ->+ case (pkeskPayloadAlgorithm payload, _pubkey recipientPKP) of+ (ECDH, ECDHPubKey _ kdfHA kdfSA) ->+ validateTable30PolicyForRecipient recipientPKP kdfHA kdfSA+ _ -> Right ()++ rsaUnwrap privateKey encryptedSessionMaterial = do+ decrypted <-+ liftIO+ ( P15.decryptSafer privateKey encryptedSessionMaterial+ :: IO (Either RSATypes.Error B.ByteString)+ )+ case decrypted of+ Left err -> fail ("RSA PKESK decrypt failed: " ++ show err)+ Right decoded -> pure decoded++ normalizePKESKv6RSAEsk+ :: RSATypes.PrivateKey+ -> B.ByteString+ -> Either String B.ByteString+ normalizePKESKv6RSAEsk privateKey esk = do+ when (B.length esk < 2) $+ Left "PKESKv6 RSA ESK is too short to contain an MPI"+ let mpiBits =+ fromIntegral (B.index esk 0) `shiftL` 8+ + fromIntegral (B.index esk 1)+ mpiLen = (mpiBits + 7) `div` 8+ when (B.length esk /= 2 + mpiLen) $+ Left+ ( "PKESKv6 RSA ESK MPI length mismatch: expected "+ ++ show (2 + mpiLen)+ ++ " octets, got "+ ++ show (B.length esk)+ )+ let mpiPayload = B.drop 2 esk+ when (mpiBits > 0) $ do+ when (B.null mpiPayload) $+ Left+ "PKESKv6 RSA ESK MPI has non-zero bit length but empty payload"+ let firstOctet = B.head mpiPayload+ actualBits =+ (B.length mpiPayload - 1) * 8+ + (8 - countLeadingZeros firstOctet)+ when (actualBits /= mpiBits) $+ Left+ ( "PKESKv6 RSA ESK MPI bit-length mismatch: declared "+ ++ show mpiBits+ ++ ", actual "+ ++ show actualBits+ )+ let modulusLen = rsaModulusBytes privateKey+ when (B.length mpiPayload > modulusLen) $+ Left+ ( "PKESKv6 RSA ESK MPI payload exceeds recipient modulus size: "+ ++ show (B.length mpiPayload)+ ++ " > "+ ++ show modulusLen+ )+ pure (leftPadTo modulusLen mpiPayload)++ ecdhUnwrap recipientCtx pka mpis privateKey = do+ recipientPKP <-+ maybe+ ( fail+ "ECDH PKESK unwrap requires recipient PKPayload context; use conduitDecrypt with DecryptWithKeyring or DecryptWithUnwrapCandidatesCallback"+ )+ pure+ (pkeskRecipientPKPayload recipientCtx)+ case _pubkey recipientPKP of+ ECDHPubKey ecdhPub kdfHA kdfSA -> do+ (ephemeralBytes, wrappedSessionKeyBytes) <-+ either fail pure (parseECDHPKESKMPIs mpis)+ sharedSecret <-+ case ecdhPub of+ ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _)) -> do+ ephPoint <- parseUncompressedPointForCurve curve ephemeralBytes+ pure+ ( BA.convert+ (ECCDH.getShared curve (ECDSA.private_d privateKey) ephPoint)+ :: B.ByteString+ )+ EdDSAPubKey EdSigningCurve25519 _ -> do+ recipientSecretRaw <-+ either fail pure (resolveX25519SecretRaw recipientPKP privateKey)+ recipientSecret <-+ either fail pure+ . first show+ . CE.eitherCryptoError+ $ C25519.secretKey recipientSecretRaw+ ephBytes <-+ either fail pure (normalizeX25519EphemeralPublic ephemeralBytes)+ ephPub <-+ either fail pure+ . first show+ . CE.eitherCryptoError+ $ C25519.publicKey ephBytes+ pure . BA.convert $ C25519.dh ephPub recipientSecret+ EdDSAPubKey EdSigningCurve448 _ ->+ fail+ "legacy ECDH PKESK unwrap does not support Curve448Legacy recipients"+ _ ->+ fail+ "ECDH PKESK unwrap requires recipient ECDH public key to be ECDSA or X25519-compatible"+ param <-+ either+ fail+ pure+ (buildECDHKDFParam recipientPKP pka ecdhPub kdfHA kdfSA)+ kek <-+ either fail pure (deriveECDHKek kdfHA kdfSA sharedSecret param)+ let wrappedCandidates =+ candidateWrappedRFC3394CiphertextsForLegacyECDH+ (LegacyECDHWrappedRFC3394Ciphertext wrappedSessionKeyBytes)+ validUnwraps =+ [ material+ | wrappedCandidate <- wrappedCandidates+ , Right decoded <-+ [ aesKeyUnwrapRFC3394+ kdfSA+ kek+ (unLegacyECDHWrappedRFC3394Ciphertext wrappedCandidate)+ ]+ , Right material <- [parseLegacyECDHDecodedSessionMaterial decoded]+ ]+ case validUnwraps of+ (material : _) -> pure (encodeLegacyECDHSessionMaterial material)+ [] ->+ case aesKeyUnwrapRFC3394 kdfSA kek wrappedSessionKeyBytes of+ Left err -> fail err+ Right _ ->+ fail+ "legacy ECDH wrapped session key decrypted but decoded session material is malformed"+ _ ->+ fail+ "ECDH PKESK unwrap requires recipient PKPayload with ECDHPubKey parameters"++ ecdhUnwrapV6 recipientCtx pka esk privateKey = do+ recipientPKP <-+ maybe+ ( fail+ "ECDH PKESKv6 unwrap requires recipient PKPayload context; use conduitDecrypt with DecryptWithKeyring or DecryptWithUnwrapCandidatesCallback"+ )+ pure+ (pkeskRecipientPKPayload recipientCtx)+ if pka == X25519+ then do+ recipientSecretRaw <-+ either fail pure (resolveX25519SecretRaw recipientPKP privateKey)+ v6X25519Unwrap recipientPKP recipientSecretRaw esk+ else+ if pka == X448+ then+ fail+ "X448 PKESKv6 unwrap requires an X448PrivateKey recipient secret key and recipient PKPayload context"+ else case _pubkey recipientPKP of+ ECDHPubKey ecdhPub kdfHA kdfSA -> do+ (ephemeralBytes, wrappedSessionKeyBytes) <-+ either fail pure (parsePKESKv6ECDHEsk pka esk)+ case ecdhPub of+ ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _)) -> do+ ephPoint <- parseUncompressedPointForCurve curve ephemeralBytes+ let sharedSecret =+ BA.convert+ (ECCDH.getShared curve (ECDSA.private_d privateKey) ephPoint)+ :: B.ByteString+ param <-+ either+ fail+ pure+ (buildECDHKDFParam recipientPKP pka ecdhPub kdfHA kdfSA)+ kek <-+ either fail pure (deriveECDHKek kdfHA kdfSA sharedSecret param)+ case aesKeyUnwrapRFC3394 kdfSA kek wrappedSessionKeyBytes of+ Left err -> fail err+ Right decoded -> pure decoded+ EdDSAPubKey EdSigningCurve25519 _ -> do+ recipientSecretRaw <-+ either fail pure (resolveX25519SecretRaw recipientPKP privateKey)+ recipientSecret <-+ either fail pure+ . first show+ . CE.eitherCryptoError+ $ C25519.secretKey recipientSecretRaw+ ephBytes <-+ either fail pure (normalizeX25519EphemeralPublic ephemeralBytes)+ ephPub <-+ either fail pure+ . first show+ . CE.eitherCryptoError+ $ C25519.publicKey ephBytes+ let sharedSecret = BA.convert (C25519.dh ephPub recipientSecret) :: B.ByteString+ param <-+ either+ fail+ pure+ (buildECDHKDFParam recipientPKP pka ecdhPub kdfHA kdfSA)+ let rfc6637Result =+ do+ kek <- deriveECDHKek kdfHA kdfSA sharedSecret param+ aesKeyUnwrapRFC3394 kdfSA kek wrappedSessionKeyBytes+ case rfc6637Result of+ Right decoded -> pure decoded+ Left rfc6637Err -> do+ recipientPublicRaw <-+ either fail pure (extractX25519RecipientPublic recipientPKP)+ let kekX25519 = deriveX25519Kek ephBytes recipientPublicRaw sharedSecret+ case aesKeyUnwrapRFC3394 AES128 kekX25519 wrappedSessionKeyBytes of+ Right decoded -> pure decoded+ Left x25519Err ->+ fail+ ( "ECDH PKESKv6 Curve25519 unwrap failed (RFC6637: "+ ++ rfc6637Err+ ++ ", X25519: "+ ++ x25519Err+ ++ ")"+ )+ EdDSAPubKey EdSigningCurve448 _ ->+ fail+ "ECDH PKESKv6 unwrap does not support Curve448Legacy recipients; use X448 PKESKv6 packets"+ _ ->+ fail+ "ECDH PKESKv6 unwrap requires recipient ECDH public key to be ECDSA or X25519-compatible"+ _ ->+ fail+ "ECDH PKESKv6 unwrap requires recipient PKPayload with ECDHPubKey parameters"++ ecdhUnwrapV6XDHRaw recipientCtx pka esk privateKeyRaw = do+ recipientPKP <-+ maybe+ ( fail+ "X25519/X448 PKESKv6 unwrap requires recipient PKPayload context; use conduitDecrypt with DecryptWithKeyring or DecryptWithUnwrapCandidatesCallback"+ )+ pure+ (pkeskRecipientPKPayload recipientCtx)+ case pka of+ X25519 -> v6X25519Unwrap recipientPKP (leftPadTo 32 privateKeyRaw) esk+ X448 -> v6X448Unwrap recipientPKP (leftPadTo 56 privateKeyRaw) esk+ _ ->+ fail+ ( "X25519/X448 PKESKv6 unwrap only supports X25519/X448 packets; got "+ ++ show pka+ )++ v6X25519Unwrap recipientPKP recipientSecretRaw esk = do+ recipientPublicRaw <-+ either fail pure (extractX25519RecipientPublic recipientPKP)+ (ephemeralBytes, wrappedSessionKeyBytes) <-+ either fail pure (parsePKESKv6ECDHEsk X25519 esk)+ ephBytes <-+ either fail pure (normalizeX25519EphemeralPublic ephemeralBytes)+ recipientSecret <-+ either fail pure+ . first show+ . CE.eitherCryptoError+ $ C25519.secretKey (leftPadTo 32 recipientSecretRaw)+ ephPub <-+ either fail pure+ . first show+ . CE.eitherCryptoError+ $ C25519.publicKey ephBytes+ let sharedSecret = BA.convert (C25519.dh ephPub recipientSecret) :: B.ByteString+ kek = deriveX25519Kek ephBytes recipientPublicRaw sharedSecret+ case aesKeyUnwrapRFC3394 AES128 kek wrappedSessionKeyBytes of+ Left err -> fail err+ Right decoded -> pure decoded++ extractX25519RecipientPublic recipientPKP =+ case _pubkey recipientPKP of+ EdDSAPubKey EdSigningCurve25519 point ->+ normalizeX25519EphemeralPublic (edPointBytes point)+ ECDHPubKey (EdDSAPubKey EdSigningCurve25519 point) _ _ ->+ normalizeX25519EphemeralPublic (edPointBytes point)+ other ->+ Left+ ( "X25519 PKESKv6 unwrap requires an X25519 recipient public key, got "+ ++ show other+ )++ extractX448RecipientPublic recipientPKP =+ case _pubkey recipientPKP of+ EdDSAPubKey EdSigningCurve448 point ->+ normalizeX448EphemeralPublic (edPointBytes point)+ ECDHPubKey (EdDSAPubKey EdSigningCurve448 point) _ _ ->+ normalizeX448EphemeralPublic (edPointBytes point)+ other ->+ Left+ ( "X448 PKESKv6 unwrap requires an X448 recipient public key, got "+ ++ show other+ )++ resolveX25519SecretRaw recipientPKP privateKey = do+ recipientPublicRaw <- extractX25519RecipientPublic recipientPKP+ let secretBE = leftPadTo 32 (i2osp (ECDSA.private_d privateKey))+ candidates = [secretBE, B.reverse secretBE]+ matchesCandidate candidate =+ case CE.eitherCryptoError (C25519.secretKey candidate) of+ Right sk ->+ let derivedPub = BA.convert (C25519.toPublic sk) :: B.ByteString+ in derivedPub == recipientPublicRaw+ Left _ -> False+ case filter matchesCandidate candidates of+ (candidate : _) -> Right candidate+ [] -> Right secretBE++ v6X448Unwrap recipientPKP recipientSecretRaw esk = do+ recipientPublicRaw <-+ either fail pure (extractX448RecipientPublic recipientPKP)+ (ephemeralBytes, wrappedSessionKeyBytes) <-+ either fail pure (parsePKESKv6ECDHEsk X448 esk)+ ephBytes <-+ either fail pure (normalizeX448EphemeralPublic ephemeralBytes)+ recipientSecret <-+ either fail pure+ . first show+ . CE.eitherCryptoError+ $ C448.secretKey (leftPadTo 56 recipientSecretRaw)+ ephPub <-+ either fail pure+ . first show+ . CE.eitherCryptoError+ $ C448.publicKey ephBytes+ let sharedSecret = BA.convert (C448.dh ephPub recipientSecret) :: B.ByteString+ kek = deriveX448Kek ephBytes recipientPublicRaw sharedSecret+ case aesKeyUnwrapRFC3394 AES256 kek wrappedSessionKeyBytes of+ Left err -> fail err+ Right decoded -> pure decoded++ x25519UnwrapV3 recipientCtx mpis recipientSecretRaw = do+ recipientPKP <-+ maybe+ ( fail+ "X25519 PKESKv3 unwrap requires recipient PKPayload context; use conduitDecrypt with DecryptWithKeyring or DecryptWithUnwrapCandidatesCallback"+ )+ pure+ (pkeskRecipientPKPayload recipientCtx)+ recipientPublicRaw <-+ either fail pure (extractX25519RecipientPublic recipientPKP)+ (ephemeralBytes, eskBytes) <-+ either fail pure (parseECDHPKESKMPIs mpis)+ ephBytes <-+ either fail pure (normalizeX25519EphemeralPublic ephemeralBytes)+ recipientSecret <-+ either fail pure+ . first show+ . CE.eitherCryptoError+ $ C25519.secretKey (leftPadTo 32 recipientSecretRaw)+ ephPub <-+ either fail pure+ . first show+ . CE.eitherCryptoError+ $ C25519.publicKey ephBytes+ let sharedSecret = BA.convert (C25519.dh ephPub recipientSecret) :: B.ByteString+ kek9580 = deriveX25519Kek ephBytes recipientPublicRaw sharedSecret+ -- RFC9580 interpretation: eskBytes = algo_byte || AES-KW(raw_session_key)+ rfc9580Result = do+ (sessionAlgorithm, wrappedKey) <-+ parsePKESKv3X25519EskBytes eskBytes+ rawKey <- aesKeyUnwrapRFC3394 AES128 kek9580 wrappedKey+ expectedLen <- symmetricKeyLength sessionAlgorithm+ when (B.length rawKey /= expectedLen) $+ Left+ ( "X25519 PKESKv3 unwrapped session key length mismatch for "+ ++ show sessionAlgorithm+ ++ ": expected "+ ++ show expectedLen+ ++ ", got "+ ++ show (B.length rawKey)+ )+ Right+ ( B.singleton (fromIntegral (fromFVal sessionAlgorithm))+ <> rawKey+ <> checksum16Bytes rawKey+ )+ case rfc9580Result of+ Right result -> pure result+ Left rfc9580Err ->+ -- Fallback: legacy ECDH interpretation where the full eskBytes is+ -- AES-KW(algo || key || checksum || padding). Try with the RFC9580+ -- X25519 KEK and, when available, the RFC6637 ECDH KEK derived from+ -- any ECDH parameters on the recipient public key.+ let kekPairs =+ (kek9580, AES128)+ : x25519LegacyECDHKekCandidates recipientPKP sharedSecret+ wrapped = LegacyECDHWrappedRFC3394Ciphertext eskBytes+ candidates = candidateWrappedRFC3394CiphertextsForLegacyECDH wrapped+ validResults =+ [ encodeLegacyECDHSessionMaterial material+ | (kek, kekSA) <- kekPairs+ , candidate <- candidates+ , Right decoded <-+ [ aesKeyUnwrapRFC3394+ kekSA+ kek+ (unLegacyECDHWrappedRFC3394Ciphertext candidate)+ ]+ , Right material <- [parseLegacyECDHDecodedSessionMaterial decoded]+ ]+ in case validResults of+ (result : _) -> pure result+ [] ->+ fail+ ( "X25519 PKESKv3 unwrap failed (RFC9580: "+ ++ rfc9580Err+ ++ "; legacy ECDH-style fallback also failed)"+ )++ -- \| Derive RFC6637 ECDH KEK candidates for legacy X25519 PKESKv3 fallback.+ -- Returns @(kek, kekAlgorithm)@ pairs for each plausible ECDH KDF+ -- parameterisation found on the recipient public key.+ x25519LegacyECDHKekCandidates+ :: SomePKPayload+ -> B.ByteString+ -> [(B.ByteString, SymmetricAlgorithm)]+ x25519LegacyECDHKekCandidates pkPayload sharedSecret =+ case _pubkey pkPayload of+ ECDHPubKey ecdhPub kdfHA kdfSA ->+ [ (kek, kdfSA)+ | Right param <-+ [buildECDHKDFParam pkPayload X25519 ecdhPub kdfHA kdfSA]+ , Right kek <- [deriveECDHKek kdfHA kdfSA sharedSecret param]+ ]+ _ -> []++rsaModulusBytes :: RSATypes.PrivateKey -> Int+rsaModulusBytes+ ( RSATypes.PrivateKey+ (RSATypes.PublicKey sizeField _ _)+ _+ _+ _+ _+ _+ _+ ) =+ sizeField++edPointBytes :: EdPoint -> B.ByteString+edPointBytes (PrefixedNativeEPoint (EPoint x)) = i2osp x+edPointBytes (NativeEPoint (EPoint x)) = i2osp x++parseECDHPKESKMPIs+ :: NonEmpty MPI -> Either String (B.ByteString, B.ByteString)+parseECDHPKESKMPIs (ephemeralMPI :| [wrappedMPI]) =+ Right (i2osp (unMPI ephemeralMPI), i2osp (unMPI wrappedMPI))+parseECDHPKESKMPIs _ =+ Left+ "ECDH PKESK must contain exactly two MPIs (ephemeral key, wrapped session key)"++newtype LegacyECDHWrappedRFC3394Ciphertext+ = LegacyECDHWrappedRFC3394Ciphertext+ { unLegacyECDHWrappedRFC3394Ciphertext :: B.ByteString+ }+ deriving (Eq)++newtype LegacyECDHSessionKey = LegacyECDHSessionKey {unLegacyECDHSessionKey :: B.ByteString}++newtype LegacyECDHSessionPadding = LegacyECDHSessionPadding+ {unLegacyECDHSessionPadding :: B.ByteString}++data LegacyECDHDecodedSessionMaterial+ = LegacyECDHDecodedSessionMaterial+ { legacyECDHSessionAlgorithm :: SymmetricAlgorithm+ , legacyECDHSessionKey :: LegacyECDHSessionKey+ , legacyECDHSessionPadding :: LegacyECDHSessionPadding+ }++candidateWrappedRFC3394CiphertextsForLegacyECDH+ :: LegacyECDHWrappedRFC3394Ciphertext+ -> [LegacyECDHWrappedRFC3394Ciphertext]+candidateWrappedRFC3394CiphertextsForLegacyECDH wrapped =+ let observedLen = B.length (unLegacyECDHWrappedRFC3394Ciphertext wrapped)+ plausibleWrappedLens = legacyECDHRFC3394WrappedLengths+ reconstructed =+ [ LegacyECDHWrappedRFC3394Ciphertext+ ( if observedLen == targetLen+ then unLegacyECDHWrappedRFC3394Ciphertext wrapped+ else+ leftPadTo+ targetLen+ (unLegacyECDHWrappedRFC3394Ciphertext wrapped)+ )+ | targetLen <- plausibleWrappedLens+ , targetLen >= observedLen+ ]+ in nub (wrapped : reconstructed)++legacyECDHRFC3394WrappedLengths :: [Int]+legacyECDHRFC3394WrappedLengths =+ map legacyRFC3394WrappedLenForKeyLen [16, 24, 32]+ where+ legacyRFC3394WrappedLenForKeyLen keyLen =+ let encodedLen = 1 + keyLen + 2+ paddedLen = ((encodedLen + 7) `div` 8) * 8+ in paddedLen + 8++parseLegacyECDHDecodedSessionMaterial+ :: B.ByteString -> Either String LegacyECDHDecodedSessionMaterial+parseLegacyECDHDecodedSessionMaterial decoded = do+ when (B.length decoded < 3) $+ Left "legacy ECDH decoded session material is too short"+ let sessionAlgorithm = toFVal (B.head decoded)+ sessionKeyLen <- symmetricKeyLength sessionAlgorithm+ let payload = B.tail decoded+ when (B.length payload < sessionKeyLen + 2) $+ Left+ "legacy ECDH decoded session material does not contain full key and checksum"+ let (sessionKey, checksumAndPad) = B.splitAt sessionKeyLen payload+ (checksumBytes, padBytes) = B.splitAt 2 checksumAndPad+ expectedChecksum =+ fromIntegral (B.index checksumBytes 0) `shiftL` 8+ + fromIntegral (B.index checksumBytes 1)+ actualChecksum = checksum16 sessionKey+ when (actualChecksum /= expectedChecksum) $+ Left "legacy ECDH decoded session-key checksum mismatch"+ if B.null padBytes || B.all (== 0) padBytes+ then+ Right+ ( LegacyECDHDecodedSessionMaterial+ sessionAlgorithm+ (LegacyECDHSessionKey sessionKey)+ (LegacyECDHSessionPadding padBytes)+ )+ else do+ validatePKCS7Padding padBytes+ Right+ ( LegacyECDHDecodedSessionMaterial+ sessionAlgorithm+ (LegacyECDHSessionKey sessionKey)+ (LegacyECDHSessionPadding padBytes)+ )++encodeLegacyECDHSessionMaterial+ :: LegacyECDHDecodedSessionMaterial -> B.ByteString+encodeLegacyECDHSessionMaterial+ ( LegacyECDHDecodedSessionMaterial+ sessionAlgorithm+ (LegacyECDHSessionKey sessionKey)+ _+ ) =+ B.singleton (fromIntegral (fromFVal sessionAlgorithm))+ <> sessionKey+ <> checksum16Bytes sessionKey++parsePKESKv6ECDHEsk+ :: PubKeyAlgorithm+ -> B.ByteString+ -> Either String (B.ByteString, B.ByteString)+parsePKESKv6ECDHEsk pka esk+ | pka == X25519 =+ case parseFixedEphemeralWithWrappedLen 32 esk of+ Right parsed -> Right parsed+ Left _ -> parseLenPrefixedEphemeral 32 esk+ | pka == X448 =+ case parseFixedEphemeralWithWrappedLen 56 esk of+ Right parsed -> Right parsed+ Left _ -> parseLenPrefixedEphemeral 56 esk+ | B.length esk < 33 =+ Left "PKESKv6 ECDH ESK is too short"+ | otherwise =+ let withLen =+ let ephLen = fromIntegral (B.head esk)+ rest = B.tail esk+ in if ephLen > 0 && B.length rest > ephLen+ then+ let (eph, wrapped) = B.splitAt ephLen rest+ in if validWrappedPayload wrapped+ then Just (eph, wrapped)+ else Nothing+ else Nothing+ fixed32WithWrappedLen = parseFixedEphemeralWithWrappedLenMaybe 32 esk+ fixed32 =+ let (eph, wrapped) = B.splitAt 32 esk+ in if validWrappedPayload wrapped+ then Just (eph, wrapped)+ else Nothing+ mpiWithWrappedLen =+ if B.length esk >= 4+ then+ let mpiBits =+ fromIntegral (B.index esk 0) `shiftL` 8+ + fromIntegral (B.index esk 1)+ mpiLen = (mpiBits + 7) `div` 8+ rest = B.drop (2 + mpiLen) esk+ in if mpiLen > 0 && B.length esk > 2 + mpiLen && not (B.null rest)+ then+ let eph = B.take mpiLen (B.drop 2 esk)+ wrappedLen = fromIntegral (B.head rest)+ wrapped = B.tail rest+ in if wrappedLen == B.length wrapped && validWrappedPayload wrapped+ then Just (eph, wrapped)+ else Nothing+ else Nothing+ else Nothing+ in case fixed32WithWrappedLen+ <|> withLen+ <|> fixed32+ <|> mpiWithWrappedLen of+ Just x -> Right x+ Nothing ->+ Left+ "PKESKv6 ECDH ESK could not be parsed as {ephemeral32||len||wrapped}, {len||ephemeral||wrapped}, {ephemeral32||wrapped}, or {mpi(ephemeral)||len||wrapped}"+ where+ validWrappedPayload wrapped = B.length wrapped >= 24 && B.length wrapped `mod` 8 == 0++ parseFixedEphemeralWithWrappedLenMaybe ephLen payload =+ let (eph, rest) = B.splitAt ephLen payload+ in if B.length rest >= 2+ then+ let wrappedLen = fromIntegral (B.head rest)+ wrapped = B.tail rest+ in if wrappedLen == B.length wrapped && validWrappedPayload wrapped+ then Just (eph, wrapped)+ else Nothing+ else Nothing++ parseFixedEphemeralWithWrappedLen ephLen payload =+ maybe+ ( Left+ ( "PKESKv6 XDH ESK expected {ephemeral"+ ++ show ephLen+ ++ "||len||wrapped} framing"+ )+ )+ Right+ (parseFixedEphemeralWithWrappedLenMaybe ephLen payload)++ parseLenPrefixedEphemeral expectedLen payload =+ if B.null payload+ then Left "PKESKv6 XDH ESK is empty"+ else+ let ephLen = fromIntegral (B.head payload)+ rest = B.tail payload+ in if ephLen == expectedLen && B.length rest > ephLen+ then+ let (eph, wrapped) = B.splitAt ephLen rest+ in if validWrappedPayload wrapped+ then Right (eph, wrapped)+ else Left "PKESKv6 XDH ESK wrapped payload has invalid length"+ else+ Left+ ( "PKESKv6 XDH ESK expected "+ ++ show expectedLen+ ++ "-octet ephemeral value"+ )++normalizeX25519EphemeralPublic+ :: B.ByteString -> Either String B.ByteString+normalizeX25519EphemeralPublic =+ normalizeMontgomeryPublic+ 32+ "invalid X25519 ephemeral public key length/prefix: "++normalizeX448EphemeralPublic+ :: B.ByteString -> Either String B.ByteString+normalizeX448EphemeralPublic =+ normalizeMontgomeryPublic+ 56+ "invalid X448 ephemeral public key length/prefix: "++parseUncompressedPointForCurve+ :: MonadFail m => ECCT.Curve -> B.ByteString -> m ECCT.Point+parseUncompressedPointForCurve curve bs+ | B.length bs < 1 = fail "ECDH ephemeral point is empty"+ | Just expectedLen <- expectedUncompressedPointLength curve+ , B.length bs /= expectedLen =+ fail+ ( "ECDH ephemeral point has invalid length for recipient curve: expected "+ ++ show expectedLen+ ++ ", got "+ ++ show (B.length bs)+ )+ | B.head bs /= 0x04 =+ fail "ECDH ephemeral point must be uncompressed (0x04)"+ | otherwise =+ let xy = B.tail bs+ in if odd (B.length xy)+ then fail "ECDH ephemeral point has malformed coordinate length"+ else+ let (xb, yb) = B.splitAt (B.length xy `div` 2) xy+ in pure (ECCT.Point (os2ip xb) (os2ip yb))++expectedUncompressedPointLength :: ECCT.Curve -> Maybe Int+expectedUncompressedPointLength curve+ | curve == ECCT.getCurveByName ECCT.SEC_p256r1 = Just 65+ | curve == ECCT.getCurveByName ECCT.SEC_p384r1 = Just 97+ | curve == ECCT.getCurveByName ECCT.SEC_p521r1 = Just 133+ | otherwise = Nothing++deriveX25519Kek+ :: B.ByteString -> B.ByteString -> B.ByteString -> B.ByteString+deriveX25519Kek ephemeralPublic recipientPublic sharedSecret =+ let ikm = ephemeralPublic <> recipientPublic <> sharedSecret+ prk = extract @CHA.SHA256 B.empty ikm+ info :: B.ByteString+ info = "OpenPGP X25519"+ okm :: B.ByteString+ okm = expand @CHA.SHA256 prk info 16+ in okm++deriveX448Kek+ :: B.ByteString -> B.ByteString -> B.ByteString -> B.ByteString+deriveX448Kek ephemeralPublic recipientPublic sharedSecret =+ let ikm = ephemeralPublic <> recipientPublic <> sharedSecret+ prk = extract @CHA.SHA512 B.empty ikm+ info :: B.ByteString+ info = "OpenPGP X448"+ okm :: B.ByteString+ okm = expand @CHA.SHA512 prk info 32+ in okm++aesKeyUnwrapRFC3394+ :: SymmetricAlgorithm+ -> B.ByteString+ -> B.ByteString+ -> Either String B.ByteString+aesKeyUnwrapRFC3394 sa kek wrapped =+ withAESCipher+ "ECDH PKESK currently supports AES KEK algorithms only"+ sa+ kek+ unwrapWithCipher+ where+ unwrapWithCipher+ :: CCT.BlockCipher cipher+ => cipher+ -> Either String B.ByteString+ unwrapWithCipher cipher = do+ when (B.length wrapped < 24 || B.length wrapped `mod` 8 /= 0) $+ Left+ "ECDH wrapped session key must be at least 24 octets and a multiple of 8"+ let (a0, rBytes) = B.splitAt 8 wrapped+ rs = chunksOf8 rBytes+ when (length rs < 2) $+ Left+ "ECDH wrapped session key must contain at least two 64-bit blocks"+ (aFinal, rFinal) <- unwrapRounds cipher a0 rs+ when (aFinal /= B.replicate 8 0xA6) $+ Left "ECDH wrapped session key integrity check failed"+ Right (B.concat rFinal)++ unwrapRounds+ :: CCT.BlockCipher cipher+ => cipher+ -> B.ByteString+ -> [B.ByteString]+ -> Either String (B.ByteString, [B.ByteString])+ unwrapRounds cipher aInit rsInit = goJ 5 aInit rsInit+ where+ n = length rsInit+ goJ j aState rsState+ | j < 0 = Right (aState, rsState)+ | otherwise = do+ (a', rs') <- goI n aState rsState+ goJ (j - 1) a' rs'+ where+ goI i aCurrent rsCurrent+ | i <= 0 = Right (aCurrent, rsCurrent)+ | otherwise = do+ 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+ rsNext = (ix (i - 1) .~ rNext) rsCurrent+ goI (i - 1) aNext rsNext++chunksOf8 :: B.ByteString -> [B.ByteString]+chunksOf8 bs+ | B.null bs = []+ | otherwise =+ let (h, t) = B.splitAt 8 bs+ in h : chunksOf8 t++xorBS :: B.ByteString -> B.ByteString -> B.ByteString+xorBS a b = B.pack (B.zipWith xor a b)++decodePKESKSessionKey+ :: Maybe SymmetricAlgorithm+ -> B.ByteString+ -> Either String (SymmetricAlgorithm, B.ByteString)+decodePKESKSessionKey expectedSymAlgo encodedSessionKey =+ case decodeOpenPGPEncodedSessionKey encodedSessionKey of+ Right (symalgo, sessionKey) ->+ case expectedSymAlgo of+ Just expected+ | expected /= symalgo ->+ Left "Decrypted PKESK symmetric algorithm does not match payload"+ _ -> Right (symalgo, sessionKey)+ Left decodeErr ->+ case expectedSymAlgo of+ Nothing ->+ Left+ ( "PKESK session key material must be OpenPGP encoded when payload algorithm is unknown: "+ ++ renderEncodedSessionKeyError decodeErr+ )+ Just expected -> do+ expectedLen <- symmetricKeyLength expected+ case decodeExpectedRawOrPaddedSessionKey+ expected+ expectedLen+ encodedSessionKey of+ Left err -> Left err+ Right sessionKey -> Right (expected, sessionKey)++parsePKESKv3X25519EskBytes+ :: B.ByteString -> Either String (SymmetricAlgorithm, B.ByteString)+parsePKESKv3X25519EskBytes eskBytes = do+ when (B.length eskBytes < 2) $+ Left "PKESKv3 X25519 ESK field is too short"+ let sessionAlgorithm = toFVal (B.head eskBytes)+ wrappedSessionKeyBytes = B.tail eskBytes+ when (sessionAlgorithm `notElem` [AES128, AES192, AES256]) $+ Left+ "PKESKv3 X25519 ESK field uses unsupported symmetric algorithm"+ when+ ( B.length wrappedSessionKeyBytes < 24+ || B.length wrappedSessionKeyBytes `mod` 8 /= 0+ )+ $ Left+ "PKESKv3 X25519 wrapped session key must be at least 24 octets and a multiple of 8"+ pure (sessionAlgorithm, wrappedSessionKeyBytes)++decodeExpectedRawOrPaddedSessionKey+ :: SymmetricAlgorithm+ -> Int+ -> B.ByteString+ -> Either String B.ByteString+decodeExpectedRawOrPaddedSessionKey expected expectedLen encodedSessionKey+ | B.length encodedSessionKey == expectedLen =+ Right encodedSessionKey+ | otherwise =+ case decodeV6PaddedSessionKeyWithoutAlgo expectedLen encodedSessionKey of+ Right sessionKey -> Right sessionKey+ Left _ ->+ case decodeV6PaddedSessionKeyWithAlgo+ expected+ expectedLen+ encodedSessionKey of+ Right sessionKey -> Right sessionKey+ Left _ ->+ Left+ "PKESK raw session key length does not match payload algorithm"++decodeV6PaddedSessionKeyWithoutAlgo+ :: Int -> B.ByteString -> Either String B.ByteString+decodeV6PaddedSessionKeyWithoutAlgo expectedLen encodedSessionKey = do+ when (B.length encodedSessionKey < expectedLen + 2) $+ Left "v6 ECDH decoded session material is too short"+ let (sessionKey, rest) = B.splitAt expectedLen encodedSessionKey+ (checksumBytes, padBytes) = B.splitAt 2 rest+ expectedChecksum =+ fromIntegral (B.index checksumBytes 0) `shiftL` 8+ + fromIntegral (B.index checksumBytes 1)+ actualChecksum = checksum16 sessionKey+ when (actualChecksum /= expectedChecksum) $+ Left "v6 ECDH decoded session-key checksum mismatch"+ validatePKCS7Padding padBytes+ Right sessionKey++decodeV6PaddedSessionKeyWithAlgo+ :: SymmetricAlgorithm+ -> Int+ -> B.ByteString+ -> Either String B.ByteString+decodeV6PaddedSessionKeyWithAlgo expected expectedLen encodedSessionKey = do+ when (B.length encodedSessionKey < expectedLen + 3) $+ Left+ "v6 ECDH decoded session material with algorithm octet is too short"+ let algOctet = B.head encodedSessionKey+ when (toFVal algOctet /= expected) $+ Left "v6 ECDH decoded session material algorithm mismatch"+ let rest = B.tail encodedSessionKey+ (sessionKey, checksumAndPad) = B.splitAt expectedLen rest+ (checksumBytes, padBytes) = B.splitAt 2 checksumAndPad+ expectedChecksum =+ fromIntegral (B.index checksumBytes 0) `shiftL` 8+ + fromIntegral (B.index checksumBytes 1)+ actualChecksum = checksum16 sessionKey+ when (actualChecksum /= expectedChecksum) $+ Left "v6 ECDH decoded session-key checksum mismatch"+ validatePKCS7Padding padBytes+ Right sessionKey++validatePKCS7Padding :: B.ByteString -> Either String ()+validatePKCS7Padding padBytes+ | B.null padBytes = Right ()+ | otherwise = do+ let padLen = fromIntegral (B.last padBytes) :: Int+ when (padLen <= 0 || padLen > 8 || B.length padBytes /= padLen) $+ Left+ "v6 ECDH decoded session material has invalid PKCS#7-style padding length"+ when (B.any (/= fromIntegral padLen) padBytes) $+ Left+ "v6 ECDH decoded session material has invalid PKCS#7-style padding bytes"++symmetricKeyLength :: SymmetricAlgorithm -> Either String Int+symmetricKeyLength = first renderCipherError . keySize++checksum16 :: B.ByteString -> Word16+checksum16 =+ fromIntegral+ . B.foldl'+ (\acc octet -> (acc + fromIntegral octet) `mod` (65536 :: Integer))+ 0++checksum16Bytes :: B.ByteString -> B.ByteString+checksum16Bytes sessionKey =+ B.pack [fromIntegral (chk `shiftR` 8), fromIntegral chk] where chk = checksum16 sessionKey
Data/Conduit/OpenPGP/Keyring.hs view
@@ -2,496 +2,502 @@ -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).- {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} module Data.Conduit.OpenPGP.Keyring- ( conduitToUnknownTKs- , TypedTKConduitError(..)- , conduitToSomeTKsEither- , conduitToSomeTKsDropping- , conduitToSomeTKsDroppingEither- , conduitToTKs- , conduitToPublicTKs- , conduitToPublicViewTKs- , conduitToSecretTKs- , AuthSecretSubkeyUID(..)- , AuthSecretSubkeyAtTime(..)- , AuthSecretSubkeyRejectionReason(..)- , AuthSecretSubkeyRejectedAtTime(..)- , AuthSecretSubkeysAtReport(..)- , authSecretSubkeysAt- , authSecretSubkeysAtReport- , conduitToAuthSecretSubkeysAt- , conduitToAuthSecretSubkeysAtReport- , conduitToTKsDropping- , conduitToTKsEither- , conduitToTKsDroppingEither- , conduitToTKsWithWireRep- , conduitToTKsDroppingWithWireRep- , conduitToTKsWithWireRepEither- , conduitToTKsDroppingWithWireRepEither- , KeyringChunkParseError(..)- , sinkPublicKeyringMap- , sinkSecretKeyringMap- , publicTKToKeyring- , secretTKToKeyring- , partitionSomeTKs- ) where+ ( TypedTKConduitError (..)+ , conduitToSomeTKsEither+ , conduitToSomeTKsDroppingEither+ , AuthSecretSubkeyUID (..)+ , AuthSecretSubkeyAtTime (..)+ , AuthSecretSubkeyRejectionReason (..)+ , AuthSecretSubkeyRejectedAtTime (..)+ , AuthSecretSubkeysAtReport (..)+ , authSecretSubkeysAt+ , authSecretSubkeysAtReport+ , conduitToAuthSecretSubkeysAt+ , conduitToAuthSecretSubkeysAtReport+ , conduitToTKsEither+ , conduitToTKsDroppingEither+ , conduitToTKsWithWireRepEither+ , conduitToTKsDroppingWithWireRepEither+ , conduitDropErrorsAndNothings+ , KeyringChunkParseError (..)+ , sinkPublicKeyringMap+ , sinkSecretKeyringMap+ , publicTKToKeyring+ , secretTKToKeyring+ , partitionSomeTKs+ ) where +import Data.Bifunctor (first) import Data.Conduit import qualified Data.Conduit.List as CL-import Data.Bifunctor (first)+import Data.IxSet.Typed (empty, insert) import Data.List (find) import Data.Maybe (maybeToList) import qualified Data.Set as Set import Data.Text (Text) import Data.Time.Clock (UTCTime)-import Data.IxSet.Typed (empty, insert) import Codec.Encryption.OpenPGP.Expirations- ( isCertificationSig- , isPKTimeValidWithSelfSignatures- , isTKTimeValid- , newestByCreationTime- , signatureCreationTime- , signatureEffectiveAt- )+ ( isCertificationSig+ , isPKTimeValidWithSelfSignatures+ , isTKTimeValid+ , newestByCreationTime+ , signatureCreationTime+ , signatureEffectiveAt+ ) import Codec.Encryption.OpenPGP.KeyringParser- ( KeyringChunkParseError(..)- , anyTK- , anyTKWithWireRep- , finalizeParsingEither- , parseAChunkEither- )-import Codec.Encryption.OpenPGP.Ontology (isSubkeyBindingSig, isTrustPkt)+ ( KeyringChunkParseError (..)+ , anyTK+ , anyTKWithWireRep+ , finalizeParsingEither+ , parseAChunkEither+ )+import Codec.Encryption.OpenPGP.Ontology+ ( isSubkeyBindingSig+ , isTrustPkt+ )+import Codec.Encryption.OpenPGP.Policy+ ( defaultVerificationPolicy+ ) import Codec.Encryption.OpenPGP.SignatureQualities- ( signatureHashedSubpacketsKnown- )+ ( signatureHashedSubpacketsKnown+ ) import Codec.Encryption.OpenPGP.Signatures- ( verifyAgainstKeys- , verifySigWith- , verifyTKWith- )+ ( verifyAgainstKeys+ , verifySigWith+ , verifyTKWith+ ) import Codec.Encryption.OpenPGP.Types import Data.Conduit.OpenPGP.Keyring.Instances () data Phase- = MainKey- | Revs- | Uids- | UAts- | Subs- | SkippingBroken- deriving (Eq, Ord, Show)+ = MainKey+ | Revs+ | Uids+ | UAts+ | Subs+ | SkippingBroken+ deriving (Eq, Ord, Show) data TypedTKConduitError- = TypedTKParseError KeyringChunkParseError- | TypedTKConversionError TKConversionError- deriving (Eq, Show)---- | Deprecated: this conduit silently drops parse failures and parse-time--- omissions. Prefer 'conduitToTKsEither' and handle errors explicitly.-conduitToUnknownTKs :: Monad m => ConduitT Pkt TKUnknown m ()-conduitToUnknownTKs =- conduitToTKsEither .|- conduitDropErrorsAndNothings-{-# DEPRECATED conduitToUnknownTKs "Use conduitToTKsEither and handle Left/Maybe explicitly." #-}+ = TypedTKParseError KeyringChunkParseError+ | TypedTKConversionError TKConversionError+ deriving (Eq, Show) -- | Canonical strict typed conduit with explicit parse+conversion error channel.-conduitToSomeTKsEither ::- Monad m => ConduitT Pkt (Either TypedTKConduitError (Maybe SomeTK)) m ()+conduitToSomeTKsEither+ :: (Monad m)+ => ConduitT Pkt (Either TypedTKConduitError (Maybe SomeTK)) m () conduitToSomeTKsEither =- conduitToTKsEither .|- CL.map toTypedSomeTKEither---- | Deprecated: this conduit silently drops parse+conversion failures.--- Prefer 'conduitToSomeTKsEither' and handle errors explicitly.-conduitToSomeTKsDropping :: Monad m => ConduitT Pkt SomeTK m ()-conduitToSomeTKsDropping =- conduitToSomeTKsDroppingEither .|- conduitDropErrorsAndNothings-{-# DEPRECATED conduitToSomeTKsDropping "Use conduitToSomeTKsEither and handle Left/Maybe explicitly." #-}+ conduitToTKsEither+ .| CL.map toTypedSomeTKEither --- | Tolerant typed conduit (broken transferable-key chunks may be omitted),--- while still surfacing parse+conversion failures.-conduitToSomeTKsDroppingEither ::- Monad m => ConduitT Pkt (Either TypedTKConduitError (Maybe SomeTK)) m ()+{- | Tolerant typed conduit (broken transferable-key chunks may be omitted),+while still surfacing parse+conversion failures.+-}+conduitToSomeTKsDroppingEither+ :: (Monad m)+ => ConduitT Pkt (Either TypedTKConduitError (Maybe SomeTK)) m () conduitToSomeTKsDroppingEither =- conduitToTKsDroppingEither .|- CL.map toTypedSomeTKEither---- | Deprecated: this conduit silently drops parse+conversion failures.--- Prefer 'conduitToSomeTKsEither' and handle errors explicitly.-conduitToTKs :: Monad m => ConduitT Pkt SomeTK m ()-conduitToTKs =- conduitToUnknownTKs .|- CL.mapMaybe (either (const Nothing) Just . fromUnknownToTKEither)-{-# DEPRECATED conduitToTKs "Use conduitToSomeTKsEither and handle Left/Maybe explicitly." #-}+ conduitToTKsDroppingEither+ .| CL.map toTypedSomeTKEither -toTypedSomeTKEither ::- Either KeyringChunkParseError (Maybe TKUnknown)- -> Either TypedTKConduitError (Maybe SomeTK)+toTypedSomeTKEither+ :: Either KeyringChunkParseError (Maybe TKUnknown)+ -> Either TypedTKConduitError (Maybe SomeTK) toTypedSomeTKEither =- either- (Left . TypedTKParseError)- (\maybeUnknown ->- case maybeUnknown of- Nothing -> Right Nothing- Just unknown -> first TypedTKConversionError (Just <$> fromUnknownToTKEither unknown))--conduitToPublicTKs :: Monad m => ConduitT Pkt (TK 'PublicTK) m ()-conduitToPublicTKs =- conduitToTKs .|- CL.mapMaybe someTKToPublicTK-{-# DEPRECATED conduitToPublicTKs "Use conduitToSomeTKsEither and perform explicit projection/filtering." #-}---- | Yield public TKs from any input: native public TKs pass through,--- secret TKs are stripped to their public view-conduitToPublicViewTKs :: Monad m => ConduitT Pkt (TK 'PublicTK) m ()-conduitToPublicViewTKs =- conduitToTKs .|- CL.map someTKToPublicViewTK-{-# DEPRECATED conduitToPublicViewTKs "Use conduitToSomeTKsEither and perform explicit projection." #-}--conduitToSecretTKs :: Monad m => ConduitT Pkt (TK 'SecretTK) m ()-conduitToSecretTKs =- conduitToTKs .|- CL.mapMaybe someTKToSecretTK-{-# DEPRECATED conduitToSecretTKs "Use conduitToSomeTKsEither and perform explicit projection/filtering." #-}+ either+ (Left . TypedTKParseError)+ ( \maybeUnknown ->+ case maybeUnknown of+ Nothing -> Right Nothing+ Just unknown ->+ first+ TypedTKConversionError+ (Just <$> fromUnknownToTKEither unknown)+ ) -data AuthSecretSubkeyUID =- AuthSecretSubkeyUID+data AuthSecretSubkeyUID+ = AuthSecretSubkeyUID { authSecretSubkeyUIDValue :: Text , authSecretSubkeyUIDIsPrimary :: Bool }- deriving (Eq, Show)+ deriving (Eq, Show) -data AuthSecretSubkeyAtTime =- AuthSecretSubkeyAtTime+data AuthSecretSubkeyAtTime+ = AuthSecretSubkeyAtTime { authSecretSubkeyPrimaryKey :: KeyPkt 'SecretPkt , authSecretSubkeyValue :: KeyPkt 'SecretPkt , authSecretSubkeyUIDs :: [AuthSecretSubkeyUID] , authSecretSubkeyPrimaryUID :: Maybe Text }- deriving (Eq, Show)+ deriving (Eq, Show) data AuthSecretSubkeyRejectionReason- = AuthSecretSubkeyTKVerificationFailed- | AuthSecretSubkeyPrimaryKeyInvalidAtTime- | AuthSecretSubkeyNotSecretSubkeyPacket- | AuthSecretSubkeyNotSubkeyPacket- | AuthSecretSubkeySubkeyInvalidAtTime- | AuthSecretSubkeyMissingAuthCapability- deriving (Eq, Show)+ = AuthSecretSubkeyTKVerificationFailed+ | AuthSecretSubkeyPrimaryKeyInvalidAtTime+ | AuthSecretSubkeyNotSecretSubkeyPacket+ | AuthSecretSubkeyNotSubkeyPacket+ | AuthSecretSubkeySubkeyInvalidAtTime+ | AuthSecretSubkeyMissingAuthCapability+ deriving (Eq, Show) -data AuthSecretSubkeyRejectedAtTime =- AuthSecretSubkeyRejectedAtTime+data AuthSecretSubkeyRejectedAtTime+ = AuthSecretSubkeyRejectedAtTime { authSecretSubkeyRejectedPrimaryKey :: KeyPkt 'SecretPkt , authSecretSubkeyRejectedValue :: Maybe (KeyPkt 'SecretPkt) , authSecretSubkeyRejectedUIDs :: [AuthSecretSubkeyUID] , authSecretSubkeyRejectedPrimaryUID :: Maybe Text , authSecretSubkeyRejectedReason :: AuthSecretSubkeyRejectionReason }- deriving (Eq, Show)+ deriving (Eq, Show) -data AuthSecretSubkeysAtReport =- AuthSecretSubkeysAtReport+data AuthSecretSubkeysAtReport+ = AuthSecretSubkeysAtReport { authSecretSubkeysAccepted :: [AuthSecretSubkeyAtTime] , authSecretSubkeysRejected :: [AuthSecretSubkeyRejectedAtTime] }- deriving (Eq, Show)+ deriving (Eq, Show) -conduitToAuthSecretSubkeysAtReport ::- Monad m- => UTCTime- -> ConduitT (TK 'SecretTK) AuthSecretSubkeysAtReport m ()+conduitToAuthSecretSubkeysAtReport+ :: Monad m+ => UTCTime+ -> ConduitT (TK 'SecretTK) AuthSecretSubkeysAtReport m () conduitToAuthSecretSubkeysAtReport validationTime =- CL.map (authSecretSubkeysAtReport validationTime)+ CL.map (authSecretSubkeysAtReport validationTime) -conduitToAuthSecretSubkeysAt ::- Monad m- => UTCTime- -> ConduitT (TK 'SecretTK) AuthSecretSubkeyAtTime m ()+conduitToAuthSecretSubkeysAt+ :: Monad m+ => UTCTime+ -> ConduitT (TK 'SecretTK) AuthSecretSubkeyAtTime m () conduitToAuthSecretSubkeysAt validationTime =- CL.concatMap (authSecretSubkeysAccepted . authSecretSubkeysAtReport validationTime)+ CL.concatMap+ ( authSecretSubkeysAccepted+ . authSecretSubkeysAtReport validationTime+ ) -authSecretSubkeysAt :: UTCTime -> TK 'SecretTK -> [AuthSecretSubkeyAtTime]+authSecretSubkeysAt+ :: UTCTime -> TK 'SecretTK -> [AuthSecretSubkeyAtTime] authSecretSubkeysAt validationTime =- authSecretSubkeysAccepted . authSecretSubkeysAtReport validationTime+ authSecretSubkeysAccepted+ . authSecretSubkeysAtReport validationTime -authSecretSubkeysAtReport :: UTCTime -> TK 'SecretTK -> AuthSecretSubkeysAtReport+authSecretSubkeysAtReport+ :: UTCTime -> TK 'SecretTK -> AuthSecretSubkeysAtReport authSecretSubkeysAtReport validationTime typedTk =- case verifyTKWith (verifySigWith (verifyAgainstKeys [untyped])) (Just validationTime) typedTk of- Left _ ->- AuthSecretSubkeysAtReport- []- [ AuthSecretSubkeyRejectedAtTime- { authSecretSubkeyRejectedPrimaryKey = primaryKey- , authSecretSubkeyRejectedValue = Nothing- , authSecretSubkeyRejectedUIDs = []- , authSecretSubkeyRejectedPrimaryUID = Nothing- , authSecretSubkeyRejectedReason = AuthSecretSubkeyTKVerificationFailed- }- ]- Right verifiedTk- | not (isTKTimeValid validationTime (tkToUnknown verifiedTk)) ->- let uids = uidContextsAt validationTime (tkToUnknown verifiedTk)- primaryUid = authSecretSubkeyUIDValue <$> find authSecretSubkeyUIDIsPrimary uids- in AuthSecretSubkeysAtReport+ case verifyTKWith+ ( verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeys [untyped])+ )+ (Just validationTime)+ typedTk of+ Left _ ->+ AuthSecretSubkeysAtReport [] [ AuthSecretSubkeyRejectedAtTime { authSecretSubkeyRejectedPrimaryKey = primaryKey , authSecretSubkeyRejectedValue = Nothing- , authSecretSubkeyRejectedUIDs = uids- , authSecretSubkeyRejectedPrimaryUID = primaryUid- , authSecretSubkeyRejectedReason = AuthSecretSubkeyPrimaryKeyInvalidAtTime+ , authSecretSubkeyRejectedUIDs = []+ , authSecretSubkeyRejectedPrimaryUID = Nothing+ , authSecretSubkeyRejectedReason =+ AuthSecretSubkeyTKVerificationFailed } ]- | otherwise ->- let uids = uidContextsAt validationTime (tkToUnknown verifiedTk)- primaryUid = authSecretSubkeyUIDValue <$> find authSecretSubkeyUIDIsPrimary uids- in foldr- (\subCandidate acc ->- case classifySecretSubkeyAtTime validationTime primaryKey uids primaryUid subCandidate of- Left rejected ->- acc {authSecretSubkeysRejected = rejected : authSecretSubkeysRejected acc}- Right accepted ->- acc {authSecretSubkeysAccepted = accepted : authSecretSubkeysAccepted acc})- (AuthSecretSubkeysAtReport [] [])- (_tkSubs verifiedTk)+ Right verifiedTk+ | not (isTKTimeValid validationTime (tkToUnknown verifiedTk)) ->+ let uids = uidContextsAt validationTime (tkToUnknown verifiedTk)+ primaryUid =+ authSecretSubkeyUIDValue+ <$> find authSecretSubkeyUIDIsPrimary uids+ in AuthSecretSubkeysAtReport+ []+ [ AuthSecretSubkeyRejectedAtTime+ { authSecretSubkeyRejectedPrimaryKey = primaryKey+ , authSecretSubkeyRejectedValue = Nothing+ , authSecretSubkeyRejectedUIDs = uids+ , authSecretSubkeyRejectedPrimaryUID = primaryUid+ , authSecretSubkeyRejectedReason =+ AuthSecretSubkeyPrimaryKeyInvalidAtTime+ }+ ]+ | otherwise ->+ let uids = uidContextsAt validationTime (tkToUnknown verifiedTk)+ primaryUid =+ authSecretSubkeyUIDValue+ <$> find authSecretSubkeyUIDIsPrimary uids+ in foldr+ ( \subCandidate acc ->+ case classifySecretSubkeyAtTime+ validationTime+ primaryKey+ uids+ primaryUid+ subCandidate of+ Left rejected ->+ acc+ { authSecretSubkeysRejected =+ rejected : authSecretSubkeysRejected acc+ }+ Right accepted ->+ acc+ { authSecretSubkeysAccepted =+ accepted : authSecretSubkeysAccepted acc+ }+ )+ (AuthSecretSubkeysAtReport [] [])+ (_tkSubs verifiedTk) where untyped = tkToUnknown typedTk primaryKey = _tkPrimaryKey typedTk -classifySecretSubkeyAtTime ::- UTCTime- -> KeyPkt 'SecretPkt- -> [AuthSecretSubkeyUID]- -> Maybe Text- -> (KeyPkt 'SecretPkt, [SignaturePayload])- -> Either AuthSecretSubkeyRejectedAtTime AuthSecretSubkeyAtTime+classifySecretSubkeyAtTime+ :: UTCTime+ -> KeyPkt 'SecretPkt+ -> [AuthSecretSubkeyUID]+ -> Maybe Text+ -> (KeyPkt 'SecretPkt, [SignaturePayload])+ -> Either AuthSecretSubkeyRejectedAtTime AuthSecretSubkeyAtTime classifySecretSubkeyAtTime validationTime primaryKey uids primaryUid (subkey, sigs)- | keyPktRole subkey /= KeyPktSubkey =- Left- AuthSecretSubkeyRejectedAtTime- { authSecretSubkeyRejectedPrimaryKey = primaryKey- , authSecretSubkeyRejectedValue = Just subkey- , authSecretSubkeyRejectedUIDs = uids- , authSecretSubkeyRejectedPrimaryUID = primaryUid- , authSecretSubkeyRejectedReason = AuthSecretSubkeyNotSubkeyPacket- }- | not (isPKTimeValidWithSelfSignatures validationTime (keyPktPKPayload subkey) sigs) =- Left- AuthSecretSubkeyRejectedAtTime- { authSecretSubkeyRejectedPrimaryKey = primaryKey- , authSecretSubkeyRejectedValue = Just subkey- , authSecretSubkeyRejectedUIDs = uids- , authSecretSubkeyRejectedPrimaryUID = primaryUid- , authSecretSubkeyRejectedReason = AuthSecretSubkeySubkeyInvalidAtTime- }- | not (subkeyAuthCapableAt validationTime sigs) =- Left- AuthSecretSubkeyRejectedAtTime- { authSecretSubkeyRejectedPrimaryKey = primaryKey- , authSecretSubkeyRejectedValue = Just subkey- , authSecretSubkeyRejectedUIDs = uids- , authSecretSubkeyRejectedPrimaryUID = primaryUid- , authSecretSubkeyRejectedReason = AuthSecretSubkeyMissingAuthCapability- }- | otherwise =- Right- AuthSecretSubkeyAtTime- { authSecretSubkeyPrimaryKey = primaryKey- , authSecretSubkeyValue = subkey- , authSecretSubkeyUIDs = uids- , authSecretSubkeyPrimaryUID = primaryUid- }+ | keyPktRole subkey /= KeyPktSubkey =+ Left+ AuthSecretSubkeyRejectedAtTime+ { authSecretSubkeyRejectedPrimaryKey = primaryKey+ , authSecretSubkeyRejectedValue = Just subkey+ , authSecretSubkeyRejectedUIDs = uids+ , authSecretSubkeyRejectedPrimaryUID = primaryUid+ , authSecretSubkeyRejectedReason = AuthSecretSubkeyNotSubkeyPacket+ }+ | not+ ( isPKTimeValidWithSelfSignatures+ validationTime+ (keyPktPKPayload subkey)+ sigs+ ) =+ Left+ AuthSecretSubkeyRejectedAtTime+ { authSecretSubkeyRejectedPrimaryKey = primaryKey+ , authSecretSubkeyRejectedValue = Just subkey+ , authSecretSubkeyRejectedUIDs = uids+ , authSecretSubkeyRejectedPrimaryUID = primaryUid+ , authSecretSubkeyRejectedReason =+ AuthSecretSubkeySubkeyInvalidAtTime+ }+ | not (subkeyAuthCapableAt validationTime sigs) =+ Left+ AuthSecretSubkeyRejectedAtTime+ { authSecretSubkeyRejectedPrimaryKey = primaryKey+ , authSecretSubkeyRejectedValue = Just subkey+ , authSecretSubkeyRejectedUIDs = uids+ , authSecretSubkeyRejectedPrimaryUID = primaryUid+ , authSecretSubkeyRejectedReason =+ AuthSecretSubkeyMissingAuthCapability+ }+ | otherwise =+ Right+ AuthSecretSubkeyAtTime+ { authSecretSubkeyPrimaryKey = primaryKey+ , authSecretSubkeyValue = subkey+ , authSecretSubkeyUIDs = uids+ , authSecretSubkeyPrimaryUID = primaryUid+ } uidContextsAt :: UTCTime -> TKUnknown -> [AuthSecretSubkeyUID] uidContextsAt validationTime tk =- map- (\(uid, _) ->- AuthSecretSubkeyUID- { authSecretSubkeyUIDValue = uid- , authSecretSubkeyUIDIsPrimary = Just uid == primaryUid- })- (_tkuUIDs tk)+ map+ ( \(uid, _) ->+ AuthSecretSubkeyUID+ { authSecretSubkeyUIDValue = uid+ , authSecretSubkeyUIDIsPrimary = Just uid == primaryUid+ }+ )+ (_tkuUIDs tk) where primaryUid = primaryUIDAt validationTime (_tkuUIDs tk) -primaryUIDAt :: UTCTime -> [(Text, [SignaturePayload])] -> Maybe Text+primaryUIDAt+ :: UTCTime -> [(Text, [SignaturePayload])] -> Maybe Text primaryUIDAt validationTime uids =- snd <$> newestByCreationTime candidates+ snd <$> newestByCreationTime candidates where candidates =- [ (createdAt, uid)- | (uid, sigs) <- uids- , cert <- maybeToList (latestEffectiveCertificationAt validationTime sigs)- , signatureMarksPrimaryUID cert- , createdAt <- maybeToList (signatureCreationTime cert)- ]+ [ (createdAt, uid)+ | (uid, sigs) <- uids+ , cert <-+ maybeToList (latestEffectiveCertificationAt validationTime sigs)+ , signatureMarksPrimaryUID cert+ , createdAt <- maybeToList (signatureCreationTime cert)+ ] subkeyAuthCapableAt :: UTCTime -> [SignaturePayload] -> Bool subkeyAuthCapableAt validationTime sigs =- maybe False signatureHasAuthKeyFlag (latestEffectiveBindingSignatureAt validationTime sigs)+ maybe+ False+ signatureHasAuthKeyFlag+ (latestEffectiveBindingSignatureAt validationTime sigs) -latestEffectiveBindingSignatureAt :: UTCTime -> [SignaturePayload] -> Maybe SignaturePayload+latestEffectiveBindingSignatureAt+ :: UTCTime -> [SignaturePayload] -> Maybe SignaturePayload latestEffectiveBindingSignatureAt validationTime sigs =- snd <$>- newestByCreationTime- [ (createdAt, sig)- | sig <- sigs- , isSubkeyBindingSig sig- , signatureEffectiveAt validationTime sig- , createdAt <- maybeToList (signatureCreationTime sig)- ]+ snd+ <$> newestByCreationTime+ [ (createdAt, sig)+ | sig <- sigs+ , isSubkeyBindingSig sig+ , signatureEffectiveAt validationTime sig+ , createdAt <- maybeToList (signatureCreationTime sig)+ ] -latestEffectiveCertificationAt :: UTCTime -> [SignaturePayload] -> Maybe SignaturePayload+latestEffectiveCertificationAt+ :: UTCTime -> [SignaturePayload] -> Maybe SignaturePayload latestEffectiveCertificationAt validationTime sigs =- snd <$>- newestByCreationTime- [ (createdAt, sig)- | sig <- sigs- , isCertificationSig sig- , signatureEffectiveAt validationTime sig- , createdAt <- maybeToList (signatureCreationTime sig)- ]+ snd+ <$> newestByCreationTime+ [ (createdAt, sig)+ | sig <- sigs+ , isCertificationSig sig+ , signatureEffectiveAt validationTime sig+ , createdAt <- maybeToList (signatureCreationTime sig)+ ] signatureHasAuthKeyFlag :: SignaturePayload -> Bool signatureHasAuthKeyFlag sig =- Set.member AuthKey (signatureKeyFlags sig)+ Set.member AuthKey (signatureKeyFlags sig) signatureKeyFlags :: SignaturePayload -> Set.Set KeyFlag signatureKeyFlags sig =- foldr- (\sp acc ->- case sp of- SigSubPacket _ (KeyFlags flags) -> Set.union flags acc- _ -> acc)- Set.empty- (maybe [] id (signatureHashedSubpacketsKnown sig))+ foldr+ ( \sp acc ->+ case sp of+ SigSubPacket _ (KeyFlags flags) -> Set.union flags acc+ _ -> acc+ )+ Set.empty+ (maybe [] id (signatureHashedSubpacketsKnown sig)) signatureMarksPrimaryUID :: SignaturePayload -> Bool signatureMarksPrimaryUID sig =- any- (\sp ->- case sp of- SigSubPacket _ (PrimaryUserId True) -> True- _ -> False)- (maybe [] id (signatureHashedSubpacketsKnown sig))+ any+ ( \sp ->+ case sp of+ SigSubPacket _ (PrimaryUserId True) -> True+ _ -> False+ )+ (maybe [] id (signatureHashedSubpacketsKnown sig)) -conduitToTKsEither ::- Monad m => ConduitT Pkt (Either KeyringChunkParseError (Maybe TKUnknown)) m ()+conduitToTKsEither+ :: Monad m+ => ConduitT+ Pkt+ (Either KeyringChunkParseError (Maybe TKUnknown))+ m+ () conduitToTKsEither = conduitToTKsEither' True --- | Deprecated: this conduit silently drops parse failures and parse-time--- omissions. Prefer 'conduitToTKsDroppingEither' when tolerant parsing is--- needed, or 'conduitToTKsEither' for strict parsing.-conduitToTKsDropping :: Monad m => ConduitT Pkt TKUnknown m ()-conduitToTKsDropping =- conduitToTKsDroppingEither .|- conduitDropErrorsAndNothings-{-# DEPRECATED conduitToTKsDropping "Use conduitToTKsDroppingEither or conduitToTKsEither and handle Left/Maybe explicitly." #-}--conduitToTKsDroppingEither ::- Monad m => ConduitT Pkt (Either KeyringChunkParseError (Maybe TKUnknown)) m ()+conduitToTKsDroppingEither+ :: Monad m+ => ConduitT+ Pkt+ (Either KeyringChunkParseError (Maybe TKUnknown))+ m+ () conduitToTKsDroppingEither = conduitToTKsEither' False -conduitToTKsWithWireRep :: Monad m => ConduitT PktWithWireRep TKWithWireRep m ()-conduitToTKsWithWireRep =- conduitToTKsWithWireRepEither .|- conduitDropErrorsAndNothings-{-# DEPRECATED conduitToTKsWithWireRep "Use conduitToTKsWithWireRepEither and handle Left/Maybe explicitly." #-}--conduitToTKsWithWireRepEither ::- Monad m- => ConduitT- PktWithWireRep- (Either KeyringChunkParseError (Maybe TKWithWireRep))- m- ()+conduitToTKsWithWireRepEither+ :: Monad m+ => ConduitT+ PktWithWireRep+ (Either KeyringChunkParseError (Maybe TKWithWireRep))+ m+ () conduitToTKsWithWireRepEither = conduitToTKsWithWireRepEither' True -conduitToTKsDroppingWithWireRep ::- Monad m => ConduitT PktWithWireRep TKWithWireRep m ()-conduitToTKsDroppingWithWireRep =- conduitToTKsDroppingWithWireRepEither .|- conduitDropErrorsAndNothings-{-# DEPRECATED conduitToTKsDroppingWithWireRep "Use conduitToTKsDroppingWithWireRepEither or conduitToTKsWithWireRepEither and handle Left/Maybe explicitly." #-}--conduitToTKsDroppingWithWireRepEither ::- Monad m- => ConduitT- PktWithWireRep- (Either KeyringChunkParseError (Maybe TKWithWireRep))- m- ()+conduitToTKsDroppingWithWireRepEither+ :: Monad m+ => ConduitT+ PktWithWireRep+ (Either KeyringChunkParseError (Maybe TKWithWireRep))+ m+ () conduitToTKsDroppingWithWireRepEither = conduitToTKsWithWireRepEither' False -fakecmAccumEither ::- Monad m- => (accum -> Either e (accum, [b]))- -> (a -> accum -> Either e (accum, [b]))- -> accum- -> ConduitT a (Either e b) m ()+fakecmAccumEither+ :: Monad m+ => (accum -> Either e (accum, [b]))+ -> (a -> accum -> Either e (accum, [b]))+ -> accum+ -> ConduitT a (Either e b) m () fakecmAccumEither finalizer f initialAccum = loop initialAccum where loop accum =- await >>=- maybe- (case finalizer accum of- Left err -> yield (Left err)- Right (_, bs) -> mapM_ (yield . Right) bs)- go- where- go a = do- case f a accum of- Left err -> do- yield (Left err)- loop initialAccum- Right (accum', bs) -> do- mapM_ (yield . Right) bs- loop accum'+ await+ >>= maybe+ ( case finalizer accum of+ Left err -> yield (Left err)+ Right (_, bs) -> mapM_ (yield . Right) bs+ )+ go+ where+ go a = do+ case f a accum of+ Left err -> do+ yield (Left err)+ loop initialAccum+ Right (accum', bs) -> do+ mapM_ (yield . Right) bs+ loop accum' -conduitDropErrorsAndNothings ::- Monad m => ConduitT (Either e (Maybe a)) a m ()+conduitDropErrorsAndNothings+ :: Monad m => ConduitT (Either e (Maybe a)) a m () conduitDropErrorsAndNothings =- CL.mapMaybe (either (const Nothing) id)+ CL.mapMaybe (either (const Nothing) id) -conduitToTKsEither' ::- Monad m => Bool -> ConduitT Pkt (Either KeyringChunkParseError (Maybe TKUnknown)) m ()+conduitToTKsEither'+ :: Monad m+ => Bool+ -> ConduitT+ Pkt+ (Either KeyringChunkParseError (Maybe TKUnknown))+ m+ () conduitToTKsEither' intolerant =- CL.filter notTrustPacket .| CL.map (: []) .|- fakecmAccumEither- finalizeParsingEither- (parseAChunkEither (anyTK intolerant))- ([], Just (Nothing, anyTK intolerant))+ CL.filter notTrustPacket+ .| CL.map (: [])+ .| fakecmAccumEither+ finalizeParsingEither+ (parseAChunkEither (anyTK intolerant))+ ([], Just (Nothing, anyTK intolerant)) where notTrustPacket = not . isTrustPkt -conduitToTKsWithWireRepEither' ::- Monad m- => Bool- -> ConduitT- PktWithWireRep- (Either KeyringChunkParseError (Maybe TKWithWireRep))- m- ()+conduitToTKsWithWireRepEither'+ :: Monad m+ => Bool+ -> ConduitT+ PktWithWireRep+ (Either KeyringChunkParseError (Maybe TKWithWireRep))+ m+ () conduitToTKsWithWireRepEither' intolerant =- CL.filter notTrustPacket .| CL.map (: []) .|- fakecmAccumEither- finalizeParsingEither- (parseAChunkEither (anyTKWithWireRep intolerant))- ([], Just (Nothing, anyTKWithWireRep intolerant))+ CL.filter notTrustPacket+ .| CL.map (: [])+ .| fakecmAccumEither+ finalizeParsingEither+ (parseAChunkEither (anyTKWithWireRep intolerant))+ ([], Just (Nothing, anyTKWithWireRep intolerant)) where notTrustPacket = not . isTrustPkt . _pktValue -sinkPublicKeyringMap :: Monad m => ConduitT (TK 'PublicTK) Void m PublicKeyring+sinkPublicKeyringMap+ :: Monad m => ConduitT (TK 'PublicTK) Void m PublicKeyring sinkPublicKeyringMap = CL.fold (flip insert) empty -sinkSecretKeyringMap :: Monad m => ConduitT (TK 'SecretTK) Void m SecretKeyring+sinkSecretKeyringMap+ :: Monad m => ConduitT (TK 'SecretTK) Void m SecretKeyring sinkSecretKeyringMap = CL.fold (flip insert) empty -- | Lift a single typed TK into its kinded keyring
Data/Conduit/OpenPGP/Verify.hs view
@@ -2,98 +2,105 @@ -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).- {-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} module Data.Conduit.OpenPGP.Verify- ( VerificationMode(..)- , VerificationModeW(..)- , conduitVerify- , verifyPacketsBatch- , verifyPacketsWithModeTyped- , verifyPacketsWithMode- ) where+ ( VerificationMode (..)+ , VerificationModeW (..)+ , conduitVerify+ , verifyPacketsBatch+ , verifyPacketsWithModeTyped+ , verifyPacketsWithMode+ ) where import Data.Conduit+import qualified Data.Conduit.List as CL import Data.Time.Clock (UTCTime)-import Data.List (foldl') -import Codec.Encryption.OpenPGP.Internal (PktStreamContext(..), emptyPSC)+import Codec.Encryption.OpenPGP.Internal+ ( PktStreamContext (..)+ , emptyPSC+ )+import Codec.Encryption.OpenPGP.Policy+ ( defaultVerificationPolicy+ ) import Codec.Encryption.OpenPGP.Signatures- ( VerificationError(..)- , verifyAgainstKeyring- , verifySigWith- )+ ( VerificationError (..)+ , verifyAgainstKeyring+ , verifySigWith+ ) import Codec.Encryption.OpenPGP.Types-import qualified Data.Conduit.List as CL data VerificationMode- = VerificationStreaming- | VerificationBatch- deriving (Eq, Show)+ = VerificationStreaming+ | VerificationBatch+ deriving (Eq, Show) data VerificationModeW (mode :: VerificationMode) where- VerificationStreamingW :: VerificationModeW 'VerificationStreaming- VerificationBatchW :: VerificationModeW 'VerificationBatch+ VerificationStreamingW+ :: VerificationModeW 'VerificationStreaming+ VerificationBatchW :: VerificationModeW 'VerificationBatch -conduitVerify ::- Monad m- => PublicKeyring- -> Maybe UTCTime- -> ConduitT Pkt (Either VerificationError Verification) m ()+conduitVerify+ :: Monad m+ => PublicKeyring+ -> Maybe UTCTime+ -> ConduitT Pkt (Either VerificationError Verification) m () conduitVerify kr mt =- CL.concatMapAccum (\pkt state -> pushPacketTyped kr mt pkt state) emptyPSC+ CL.concatMapAccum+ (\pkt state -> pushPacketTyped kr mt pkt state)+ emptyPSC -verifyPacketsBatch ::- PublicKeyring- -> Maybe UTCTime- -> [Pkt]- -> [Either VerificationError Verification]+verifyPacketsBatch+ :: PublicKeyring+ -> Maybe UTCTime+ -> [Pkt]+ -> [Either VerificationError Verification] verifyPacketsBatch kr mt =- verifyPacketsBatchTyped kr mt+ verifyPacketsBatchTyped kr mt -verifyPacketsBatchTyped ::- PublicKeyring- -> Maybe UTCTime- -> [Pkt]- -> [Either VerificationError Verification]+verifyPacketsBatchTyped+ :: PublicKeyring+ -> Maybe UTCTime+ -> [Pkt]+ -> [Either VerificationError Verification] verifyPacketsBatchTyped kr mt =- reverse . snd . foldl' step (emptyPSC, [])+ reverse . snd . foldl' step (emptyPSC, []) where step (state, outputs) pkt =- let (nextState, newOutputs) = pushPacketTyped kr mt pkt state- in (nextState, reverse newOutputs ++ outputs)+ let (nextState, newOutputs) = pushPacketTyped kr mt pkt state+ in (nextState, reverse newOutputs ++ outputs) -verifyPacketsWithMode ::- Monad m- => VerificationMode- -> PublicKeyring- -> Maybe UTCTime- -> ConduitT Pkt (Either VerificationError Verification) m ()+verifyPacketsWithMode+ :: Monad m+ => VerificationMode+ -> PublicKeyring+ -> Maybe UTCTime+ -> ConduitT Pkt (Either VerificationError Verification) m () verifyPacketsWithMode VerificationStreaming kr mt =- verifyPacketsWithModeTyped VerificationStreamingW kr mt+ verifyPacketsWithModeTyped VerificationStreamingW kr mt verifyPacketsWithMode VerificationBatch kr mt =- verifyPacketsWithModeTyped VerificationBatchW kr mt+ verifyPacketsWithModeTyped VerificationBatchW kr mt -verifyPacketsWithModeTyped ::- Monad m- => VerificationModeW mode- -> PublicKeyring- -> Maybe UTCTime- -> ConduitT Pkt (Either VerificationError Verification) m ()+verifyPacketsWithModeTyped+ :: Monad m+ => VerificationModeW mode+ -> PublicKeyring+ -> Maybe UTCTime+ -> ConduitT Pkt (Either VerificationError Verification) m () verifyPacketsWithModeTyped modeW kr mt =- case modeW of- VerificationStreamingW -> conduitVerify kr mt- VerificationBatchW -> CL.consume >>= mapM_ yield . verifyPacketsBatch kr mt+ case modeW of+ VerificationStreamingW -> conduitVerify kr mt+ VerificationBatchW -> CL.consume >>= mapM_ yield . verifyPacketsBatch kr mt -pushPacketTyped ::- PublicKeyring- -> Maybe UTCTime- -> Pkt- -> PktStreamContext- -> (PktStreamContext, [Either VerificationError Verification])+pushPacketTyped+ :: PublicKeyring+ -> Maybe UTCTime+ -> Pkt+ -> PktStreamContext+ -> (PktStreamContext, [Either VerificationError Verification]) pushPacketTyped _ _ ld@LiteralDataPkt {} state = (state {lastLD = ld}, []) pushPacketTyped _ _ uid@(UserIdPkt _) state = (state {lastUIDorUAt = uid}, []) pushPacketTyped _ _ uat@(UserAttributePkt _) state = (state {lastUIDorUAt = uat}, [])@@ -102,37 +109,54 @@ pushPacketTyped _ _ sk@(SecretKeyPkt _ _) state = (state {lastPrimaryKey = sk}, []) pushPacketTyped _ _ sk@(SecretSubkeyPkt _ _) state = (state {lastSubkey = sk}, []) pushPacketTyped kr mt sig@(SignaturePkt signature) state =- case fromSignaturePayloadVerifiableSignatureV signature of- Just _ ->- ( state {lastSig = sig}- , [verifySigWith (verifyAgainstKeyring kr) sig state mt]- )- Nothing -> (state, [])-pushPacketTyped _ _ (OtherPacketPkt t _) state | t < 40 =- (state, [Left (UnknownCriticalPacketInStream t)])-pushPacketTyped _ _ (BrokenPacketPkt err t _) state | t < 40 =- (state, [Left (BrokenCriticalPacketInStream t err)])+ case fromSignaturePayloadVerifiableSignatureV signature of+ Just _ ->+ ( state {lastSig = sig}+ ,+ [ verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeyring kr)+ sig+ state+ mt+ ]+ )+ Nothing -> (state, [])+pushPacketTyped _ _ (OtherPacketPkt t _) state+ | t < 40 =+ (state, [Left (UnknownCriticalPacketInStream t)])+pushPacketTyped _ _ (BrokenPacketPkt err t _) state+ | t < 40 =+ (state, [Left (BrokenCriticalPacketInStream t err)]) pushPacketTyped _ _ pkt@(OnePassSignaturePkt _) state- | isOpeningOnePassSignature pkt = (state, [])+ | isOpeningOnePassSignature pkt = (state, []) pushPacketTyped _ _ _ state = (state, []) data VerifiableSignatureV where- VerifiableSignatureV4 :: SignaturePayloadV 'SigPayloadV4 -> VerifiableSignatureV- VerifiableSignatureV6 :: SignaturePayloadV 'SigPayloadV6 -> VerifiableSignatureV+ VerifiableSignatureV4+ :: SignaturePayloadV 'SigPayloadV4 -> VerifiableSignatureV+ VerifiableSignatureV6+ :: SignaturePayloadV 'SigPayloadV6 -> VerifiableSignatureV -fromSignaturePayloadVerifiableSignatureV ::- SignaturePayload -> Maybe VerifiableSignatureV+fromSignaturePayloadVerifiableSignatureV+ :: SignaturePayload -> Maybe VerifiableSignatureV fromSignaturePayloadVerifiableSignatureV sigPayload =- case toSomeSignaturePayload sigPayload of- SomeSignaturePayload (payload@SigPayloadV4Data {}) ->- Just (VerifiableSignatureV4 payload)- SomeSignaturePayload (payload@SigPayloadV6Data {}) ->- Just (VerifiableSignatureV6 payload)- _ -> Nothing+ case toSomeSignaturePayload sigPayload of+ SomeSignaturePayload (payload@SigPayloadV4Data {}) ->+ Just (VerifiableSignatureV4 payload)+ SomeSignaturePayload (payload@SigPayloadV6Data {}) ->+ Just (VerifiableSignatureV6 payload)+ _ -> Nothing isOpeningOnePassSignature :: Pkt -> Bool-isOpeningOnePassSignature (OnePassSignaturePkt (OPSPayloadV3Packet (OPSPayloadV3 _ _ _ _ _ False))) =- True-isOpeningOnePassSignature (OnePassSignaturePkt (OPSPayloadV6Packet (OPSPayloadV6 _ _ _ _ _ False))) =- True+isOpeningOnePassSignature+ ( OnePassSignaturePkt+ (OPSPayloadV3Packet (OPSPayloadV3 _ _ _ _ _ False))+ ) =+ True+isOpeningOnePassSignature+ ( OnePassSignaturePkt+ (OPSPayloadV6Packet (OPSPayloadV6 _ _ _ _ _ False))+ ) =+ True isOpeningOnePassSignature _ = False
bench/mark.hs view
@@ -2,105 +2,124 @@ -- Copyright © 2014-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).- {-# LANGUAGE FlexibleContexts #-} import Criterion.Main--import Codec.Encryption.OpenPGP.Serialize- ( conduitParsePktsWithWireRep- , parsePkts- , parsePktsEither- , parsePktsWithWireRep- )-import Codec.Encryption.OpenPGP.Signatures- ( verifyAgainstKeyring- , verifyAgainstKeys- , verifySigWith- , verifyTKWith- , verifyUnknownTKWith- )-import Codec.Encryption.OpenPGP.Types- ( someTKToPublicViewTK- , wireRepRef- )- import Data.Binary (get)-import Data.Conduit.OpenPGP.Keyring- ( conduitToSomeTKsEither- , conduitToTKsEither- )-import Data.Conduit.Serialization.Binary (conduitGet)-import qualified Data.IxSet.Typed as IxSet import qualified Data.ByteString.Lazy as BL-import Data.Either (rights)-import Data.Maybe (catMaybes)- import qualified Data.Conduit as DC import qualified Data.Conduit.Binary as CB import qualified Data.Conduit.List as CL+import Data.Conduit.Serialization.Binary (conduitGet)+import Data.Either (rights)+import qualified Data.IxSet.Typed as IxSet+import Data.Maybe (catMaybes) +import Codec.Encryption.OpenPGP.Policy+ ( defaultVerificationPolicy+ )+import Codec.Encryption.OpenPGP.Serialize+ ( conduitParsePktsWithWireRep+ , parsePkts+ , parsePktsEither+ , parsePktsWithWireRep+ )+import Codec.Encryption.OpenPGP.Signatures+ ( verifyAgainstKeyring+ , verifyAgainstKeys+ , verifySigWith+ , verifyTKWith+ , verifyUnknownTKWith+ )+import Codec.Encryption.OpenPGP.Types+ ( someTKToPublicViewTK+ , wireRepRef+ )+import Data.Conduit.OpenPGP.Keyring+ ( conduitToSomeTKsEither+ , conduitToTKsEither+ )+ main :: IO () main =- defaultMain- [ bgroup- "keyring"- [ bench "load keys" $ whnfIO (loadKeys "tests/data/pubring.gpg")- , bench "load keyring" $ whnfIO (loadKeyring "tests/data/pubring.gpg")- , bench "self-verify keys" $- whnfIO (selfVerifyKeys "tests/data/pubring.gpg")- , bench "self-verify keyring" $- whnfIO (selfVerifyKeyring "tests/data/pubring.gpg")+ defaultMain+ [ bgroup+ "keyring"+ [ bench "load keys" $ whnfIO (loadKeys "tests/data/pubring.gpg")+ , bench "load keyring" $+ whnfIO (loadKeyring "tests/data/pubring.gpg")+ , bench "self-verify keys" $+ whnfIO (selfVerifyKeys "tests/data/pubring.gpg")+ , bench "self-verify keyring" $+ whnfIO (selfVerifyKeyring "tests/data/pubring.gpg")+ ]+ , env (BL.readFile "tests/data/pubring.gpg") $ \pubringPayload ->+ let pubringRef = wireRepRef pubringPayload+ in bgroup+ "packet-parse"+ [ bench "parsePkts/count" $ nf (length . parsePkts) pubringPayload+ , bench "parsePktsEither/count" $+ nf+ (either (const 0) length . parsePktsEither)+ pubringPayload+ , bench "parsePktsWithWireRep/count" $+ nf+ (length . parsePktsWithWireRep pubringRef)+ pubringPayload+ , bench "conduitParsePktsWithWireRep/count" $+ whnfIO+ ( fmap+ length+ ( DC.runConduitRes $+ CB.sourceLbs pubringPayload+ DC..| conduitParsePktsWithWireRep Nothing+ DC..| CL.consume+ )+ )+ ] ]- , env (BL.readFile "tests/data/pubring.gpg") $ \pubringPayload ->- let pubringRef = wireRepRef pubringPayload- in- bgroup- "packet-parse"- [ bench "parsePkts/count" $ nf (length . parsePkts) pubringPayload- , bench "parsePktsEither/count" $- nf- (either (const 0) length . parsePktsEither)- pubringPayload- , bench "parsePktsWithWireRep/count" $- nf- (length . parsePktsWithWireRep pubringRef)- pubringPayload- , bench "conduitParsePktsWithWireRep/count" $- whnfIO- (fmap- length- (DC.runConduitRes $- CB.sourceLbs pubringPayload DC..| conduitParsePktsWithWireRep Nothing DC..|- CL.consume))- ]- ] where loadKeys fp =- fmap- (catMaybes . rights)- (DC.runConduitRes $- CB.sourceFile fp DC..| conduitGet get DC..| conduitToTKsEither DC..|- CL.consume)+ fmap+ (catMaybes . rights)+ ( DC.runConduitRes $+ CB.sourceFile fp+ DC..| conduitGet get+ DC..| conduitToTKsEither+ DC..| CL.consume+ ) loadKeyring fp =- fmap- (sinkFromSomeTKs . rights)- (DC.runConduitRes $- CB.sourceFile fp DC..| conduitGet get DC..| conduitToSomeTKsEither DC..|- CL.consume)+ fmap+ (sinkFromSomeTKs . rights)+ ( DC.runConduitRes $+ CB.sourceFile fp+ DC..| conduitGet get+ DC..| conduitToSomeTKsEither+ DC..| CL.consume+ ) selfVerifyKeys fp =- fmap- (\ks ->- mapM (verifyUnknownTKWith (verifySigWith (verifyAgainstKeys ks)) Nothing) ks)- (loadKeys fp)+ fmap+ ( \ks ->+ mapM+ ( verifyUnknownTKWith+ (verifySigWith defaultVerificationPolicy (verifyAgainstKeys ks))+ Nothing+ )+ ks+ )+ (loadKeys fp) selfVerifyKeyring fp =- fmap- (\kr ->- mapM- (verifyTKWith (verifySigWith (verifyAgainstKeyring kr)) Nothing)- (IxSet.toList kr))- (loadKeyring fp)+ fmap+ ( \kr ->+ mapM+ ( verifyTKWith+ (verifySigWith defaultVerificationPolicy (verifyAgainstKeyring kr))+ Nothing+ )+ (IxSet.toList kr)+ )+ (loadKeyring fp) sinkFromSomeTKs =- IxSet.fromList .- map someTKToPublicViewTK .- catMaybes+ IxSet.fromList+ . map someTKToPublicViewTK+ . catMaybes
hOpenPGP.cabal view
@@ -1,6 +1,6 @@ Cabal-version: 3.4 Name: hOpenPGP-Version: 3.0.2.2+Version: 3.1 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@@ -332,4 +332,4 @@ source-repository this type: git location: https://salsa.debian.org/clint/hOpenPGP.git- tag: v3.0.2.2+ tag: v3.1
tests/Tests/Common.hs view
@@ -2,1537 +2,1927 @@ -- 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 fromUnknownToTKEither 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-+{-# 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 qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA+import Codec.Encryption.OpenPGP.ASCIIArmor.Types+ ( Armor (..)+ , ArmorType (..)+ )+import Control.Error.Util (hush)+import Control.Monad (join, unless, void)+import Control.Monad.Trans.Resource (ResourceT)+import qualified Crypto.Error as CE+import qualified Crypto.Hash as CH+import qualified Crypto.Hash.Algorithms as CHA+import Crypto.KDF.HKDF (expand, extract)+import Crypto.Number.Serialize (os2ip)+import qualified Crypto.PubKey.ECC.ECDSA as ECDSA+import qualified Crypto.PubKey.ECC.Types as ECCT+import qualified Crypto.PubKey.Ed25519 as Ed25519+import qualified Crypto.PubKey.Ed448 as Ed448+import qualified Crypto.PubKey.RSA as RSA+import qualified Crypto.PubKey.RSA.PKCS15 as P15+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.ByteArray as BA+import qualified Data.ByteString as B+import qualified Data.ByteString.Base16.Lazy as B16L+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Lazy.Char8 as BLC8+import Data.Char (toUpper)+import qualified Data.Conduit as DC+import qualified Data.Conduit.Binary as CB+import qualified Data.Conduit.List as CL+import Data.Conduit.Serialization.Binary (conduitGet)+import Data.IORef (IORef, modifyIORef', newIORef, readIORef)+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 System.IO.Unsafe (unsafePerformIO)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit+ ( Assertion+ , assertBool+ , assertEqual+ , assertFailure+ , testCase+ )+import qualified "crypton" Crypto.Cipher.AES as AES+import qualified "crypton" Crypto.Cipher.Types as CCT++import Codec.Encryption.OpenPGP.Arbitrary ()+import Codec.Encryption.OpenPGP.Compression (decompressPkt)+import Codec.Encryption.OpenPGP.Encrypt+ ( PKESKSessionMaterial+ , encodeOpenPGPSessionMaterial+ , mkPKESKSessionMaterial+ )+import Codec.Encryption.OpenPGP.Fingerprint+ ( eightOctetKeyID+ , fingerprint+ )+import Codec.Encryption.OpenPGP.Internal+ ( curveFromCurve+ , curveToCurveoidBS+ , emptyPSC+ , lastPrimaryKey+ , lastSubkey+ , lastUIDorUAt+ )+import Codec.Encryption.OpenPGP.KeySelection (parseFingerprint)+import Codec.Encryption.OpenPGP.Message+ ( ClearPayload+ , EncryptMessageOptions (..)+ , EncryptedPayload+ , MessageError (..)+ , Passphrase+ , RecoveredSessionMaterial (..)+ , SessionMaterialExposure (..)+ , VersionedPKPayload+ , asV4PKPayload+ , asV6PKPayload+ , encryptMessage+ )+import Codec.Encryption.OpenPGP.Policy+ ( DecryptPolicy+ , defaultDecryptPolicy+ )+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 (..)+ , renderSignError+ , renderVerificationError+ , signCertRevocationWithRSA+ , signCertificationWithRSA+ , signDataWithEd25519+ , signDataWithRSA+ , signDirectKeyWithRSA+ , signKeyRevocationWithRSA+ , signSubkeyRevocationWithRSA+ )+import Codec.Encryption.OpenPGP.Types+import Data.Conduit.OpenPGP.Compression (conduitDecompress)+import Data.Conduit.OpenPGP.Decrypt+ ( DecryptKeyResolution (..)+ , DecryptOptions (..)+ , DecryptOutcome (..)+ , PKESKRecipientKey (..)+ )+import qualified Data.Conduit.OpenPGP.Decrypt as DCD+import Data.Conduit.OpenPGP.Keyring+ ( conduitToSomeTKsEither+ , partitionSomeTKs+ , sinkPublicKeyringMap+ )+import Data.Conduit.OpenPGP.Message+ ( VerificationMode (..)+ , VerificationOptions (..)+ , VerificationPolicy (..)+ , defaultVerificationOptions+ , verifyMessage+ , verifyMessagePackets+ )++-- 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 (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ rid pka _))) keyInfos+ | isWildcardEightOctetKeyId rid =+ listToMaybe+ [ keyInfo | keyInfo <- keyInfos, supportsPKESKAlgorithm pka keyInfo+ ]+ | otherwise =+ listToMaybe+ [ keyInfo+ | keyInfo <- keyInfos+ , supportsPKESKAlgorithm pka keyInfo+ , matchesEightOctetRecipientKeyId rid keyInfo+ ]+selectRecipientKeyInfo _ keyInfos = listToMaybe keyInfos++isWildcardEightOctetKeyId :: EightOctetKeyId -> Bool+isWildcardEightOctetKeyId (EightOctetKeyId rid) =+ BL.length rid == 8 && BL.all (== 0) rid++matchesEightOctetRecipientKeyId+ :: EightOctetKeyId -> PKESKRecipientKey -> Bool+matchesEightOctetRecipientKeyId (EightOctetKeyId rid) keyInfo =+ case pkeskRecipientPKPayload keyInfo of+ Nothing -> False+ Just pkp ->+ let fingerprintBytes = unFingerprint (fingerprint pkp)+ in BL.length fingerprintBytes >= 8+ && BL.drop (BL.length fingerprintBytes - 8) fingerprintBytes == rid++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..| conduitToSomeTKsEither+ DC..| CL.map (fmap someTKToPublicViewTK . join . hush)+ DC..| CL.catMaybes+ 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 fromUnknownToTKEither 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+ EdSigningCurve25519+ ( 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+ EdSigningCurve25519+ (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+ EdSigningCurve448+ ( 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+ EdSigningCurve448+ (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 (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ rid pka _))) keyInfos+ | isWildcardEightOctetKeyId rid =+ listToMaybe+ [ keyInfo | keyInfo <- keyInfos, supportsPKESKAlgorithm pka keyInfo+ ]+ | otherwise =+ listToMaybe+ [ keyInfo+ | keyInfo <- keyInfos+ , supportsPKESKAlgorithm pka keyInfo+ , matchesEightOctetRecipientKeyId 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"++{-# NOINLINE secretKeyInfoCacheRef #-}+secretKeyInfoCacheRef+ :: IORef [(([Pkt], BL.ByteString), [PKESKRecipientKey])]+secretKeyInfoCacheRef = unsafePerformIO (newIORef [])++collectSecretKeyInfos+ :: [Pkt] -> BL.ByteString -> IO [PKESKRecipientKey]+collectSecretKeyInfos pkts passphrase = do+ cache <- readIORef secretKeyInfoCacheRef+ case lookup (pkts, passphrase) cache of+ Just cached -> pure cached+ Nothing -> do+ keyInfos <- computeSecretKeyInfos+ modifyIORef'+ secretKeyInfoCacheRef+ (((pkts, passphrase), keyInfos) :)+ pure keyInfos+ where+ computeSecretKeyInfos = 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++ 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
tests/Tests/Encryption.hs view
@@ -2,5090 +2,7236 @@ -- 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.Encryption (encryptionAndCompressionTests) where--import qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA-import Codec.Encryption.OpenPGP.ASCIIArmor.Types (Armor(..))-import Codec.Encryption.OpenPGP.BlockCipher (keySize, withSymmetricCipher)-import Codec.Encryption.OpenPGP.CFB-import Codec.Encryption.OpenPGP.Compression (CompressionError(..), compressPkts, decompressPkt)-import Codec.Encryption.OpenPGP.Encrypt- ( PKESKEncryptError(InvalidRecipientIdentifier, InvalidRecipientKeyMaterial, PayloadBuildFailure, RecipientCapabilitySelectionFailure, RecipientKdfFailure, UnsupportedRecipientAlgorithm)- , RecipientCapabilityError(RecipientCapabilityMissingSEIPDv1Support, RecipientCapabilityNoCommonAEADAlgorithms, RecipientCapabilityNoCommonSymmetricAlgorithms, RecipientCapabilityNoEncryptableKeyMaterialInTK)- , RecipientCapabilityNegotiationMode(..)- , PassphraseSKESKVersionPolicy(..)- , PassphraseEncryptRequest(..)- , EncryptCompatibilityProfile(..)- , RecipientCapabilities(..)- , RecipientEncryptionTargetRejected(..)- , RecipientEncryptionTargetsReport(..)- , RecipientTargetRejectionReason(..)- , RecipientTargetSelectionPolicy(..)- , RecipientPKESKVersionStrategy(..)- , RecipientEncryptionTarget(..)- , RecipientPayloadShape(..)- , RecipientEncryptRequest(..)- , RecipientEncryptRequestOverrides(..)- , defaultRecipientPayloadShape- , RecipientEncryptResult(..)- , PKESKVersionPolicy(..)- , pkeskSessionAlgorithm- , pkeskV3SessionMaterial- , buildPKESKv3PayloadForRecipient- , buildPKESKPayloadForRecipient- , canonicalizePKESKRecipientId- , encryptSEIPDv2Payload- , encryptForRecipients- , encryptForRecipientsLegacy- , encryptForRecipientsWithCapabilityNegotiation- , encryptPassphraseWithPolicy- , recipientCapabilitiesFromSubpacketPayloads- , recipientEncryptionTargetFromTKAtTimestamp- , recipientEncryptionTargetFromTKAtTimestampWithPolicy- , recipientEncryptionTargetsFromTKAtTimestamp- , recipientEncryptionTargetsReportFromTKAtTimestamp- , recipientEncryptionTargetFromTK- , recipientEncryptionTargetsFromTK- , recipientEncryptionTarget- , recipientEncryptionTargetWithCapabilities- , recipientEncryptionTargetWithStrategy- , recipientVersionStrategyForProfile- )-import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)-import Codec.Encryption.OpenPGP.Internal.HOBlockCipher (HOBlockCipher(..))-import Codec.Encryption.OpenPGP.Internal (point2MBS)-import Codec.Encryption.OpenPGP.Message- ( SessionMaterialExposure(..)- , encryptedPayloadBytes- , mkClearPayload- , mkPassphrase- )-import Codec.Encryption.OpenPGP.Policy- ( OpenPGPPolicy(..)- , OpenPGPRFC(..)- , defaultDecryptPolicy- , defaultPolicy- , lenientDecryptPolicy- , policyForRFC- )-import Codec.Encryption.OpenPGP.S2K- ( EncodedSessionKeyError(..)- , S2KError(..)- , decodeOpenPGPEncodedSessionKey- , renderS2KError- , skesk2SessionKey- , string2Key- )-import Codec.Encryption.OpenPGP.Serialize (parsePkts)-import Codec.Encryption.OpenPGP.SecretKey- ( decryptPrivateKey- , encryptPrivateKeyWithPolicyAndSaltAndIV- )-import Codec.Encryption.OpenPGP.Types-import Control.Exception.Base (SomeException, catch, try)-import Control.Monad (void)-import Control.Monad.Trans.Resource (ResourceT)-import qualified Crypto.Error as CE-import Crypto.Number.ModArithmetic (inverse)-import Crypto.Number.Serialize (i2osp, os2ip)-import qualified Crypto.PubKey.Curve25519 as C25519-import qualified Crypto.PubKey.Curve448 as C448-import qualified Crypto.PubKey.ECC.DH as ECCDH-import qualified Crypto.PubKey.ECC.ECDSA as ECDSA-import qualified Crypto.PubKey.ECC.Generate as ECCGen-import qualified Crypto.PubKey.ECC.Types as ECCT-import Crypto.PubKey.ECC.Prim (pointBaseMul)-import qualified Crypto.PubKey.RSA.PKCS15 as P15-import qualified Crypto.PubKey.RSA as RSA-import Data.Binary.Get- ( Get- )-import qualified Data.ByteArray as BA-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 qualified Data.Conduit as DC-import Data.Conduit (fuseBoth)-import qualified Data.Conduit.Binary as CB-import qualified Data.Conduit.List as CL-import Data.Conduit.OpenPGP.Compression (conduitCompress)-import Data.Conduit.OpenPGP.Decrypt- ( DecryptKeyResolution(..)- , DecryptOptions(..)- , DecryptOutcome(..)- , PKESKRecipientKey(..)- )-import qualified Data.Conduit.OpenPGP.Decrypt as DCD-import Data.Conduit.OpenPGP.Keyring- ( conduitToSomeTKsDropping- , conduitToSomeTKsDroppingEither- , conduitToSomeTKsEither- , conduitToTKsDropping- , conduitToTKsDroppingEither- , conduitToTKsEither- , conduitToUnknownTKs- )-import Data.Conduit.Serialization.Binary (conduitGet)-import Data.Binary (get, put)-import Data.Binary.Put (putWord16be, runPut)-import Data.Word (Word16)-import Data.List (find, isInfixOf)-import Data.List.NonEmpty (NonEmpty(..))-import qualified Data.List.NonEmpty as NE-import Data.Maybe (listToMaybe)-import qualified Data.Set as Set-import Test.Tasty (TestTree, testGroup)-import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, testCase)-import Tests.Common- ( aesKeyWrapRFC3394ForTest- , armorPayload- , assertX25519EskShape- , assertX448EskShape- , buildCurve25519LegacyKdfParamForTest- , buildECDHKDFParamForTest- , cgp- , collectSecretKeyInfos- , conduitDecrypt- , conduitDecryptChecked- , conduitDecryptCheckedWithDecryptPolicy- , conduitDecryptWithCandidatesCallbackAndPolicy- , conduitDecryptWithDecryptPolicy- , conduitDecryptWithPKESKContext- , deriveECDHKekForTest- , deriveX25519KekForTest- , deriveX448KekForTest- , doPkeyAndSkeyMatch- , encodeChecksum16- , encryptMessageDefault- , fixturePath- , forceVersionedRecipientIdentifier- , isPrecedingESK- , loadArmor- , loadDeterministicEd25519Signer- , loadSEIPDv2FixtureWithV4Secret- , loadUnencryptedRsaSigner- , mkPKESKSessionMaterialOrFail- , prependUnusableLatestPKESK- , readFixtureLazy- , readFixtureStrict- , readFixturePackets- , readPKIPassphrase- , reorderPrecedingPKESKs- , runGet- , selectRecipientKeyInfo- , selectRecipientKeyInfoByRawRecipientId- , setKeyTimestamp- , setKeyVersion- , signDirectKeyWithRSAExtrasAt- , signSubkeyBindingWithRSAExtrasAt- , signSubkeyRevocationWithRSAAt- , testEncodeOpenPGPSessionMaterial- , testParsedRSASecretKeyPKCS15DecryptNotMessageNotRecognized- , testSEIPDv2ForV4KeyArmor- , testSEIPDv2TwoRecipientsArmor- , testSEIPDv2ThreeRecipientsArmor- )--mkLegacySymmetricCase :: (String, Bool, FilePath, FilePath, BL.ByteString) -> TestTree-mkLegacySymmetricCase (label, expectSEIPDv1, encFile, passFile, cleartext) =- testCase label (testLegacySymmetricEncryption expectSEIPDv1 encFile passFile cleartext)--legacySymmetricCases :: [(String, Bool, FilePath, FilePath, BL.ByteString)]-legacySymmetricCases =- [ ("Symmetric Encryption simple S2K SHA1 3DES, no MDC", False, "encryption-sym-3des-s2k0.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted S2K SHA1 3DES, no MDC", False, "encryption-sym-3des.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption simple S2K SHA1 3DES", True, "encryption-sym-3des-mdc-s2k0.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted S2K SHA1 3DES", True, "encryption-sym-3des-mdc.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption simple S2K SHA1 CAST5, no MDC", False, "encryption-sym-cast5-s2k0.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted S2K SHA1 CAST5, no MDC", False, "encryption-sym-cast5.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption simple S2K SHA1 CAST5", True, "encryption-sym-cast5-mdc-s2k0.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted S2K SHA1 CAST5", True, "encryption-sym-cast5-mdc.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption simple S2K SHA1 Blowfish, no MDC", False, "encryption-sym-blowfish-s2k0.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted S2K SHA1 Blowfish, no MDC", False, "encryption-sym-blowfish.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption simple S2K SHA1 Blowfish", True, "encryption-sym-blowfish-mdc-s2k0.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted S2K SHA1 Blowfish", True, "encryption-sym-blowfish-mdc.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption simple S2K SHA1 AES128", True, "encryption-sym-aes128-s2k0.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted S2K SHA1 AES128", True, "encryption-sym-aes128.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption simple S2K SHA1 AES192", True, "encryption-sym-aes192-s2k0.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted S2K SHA1 AES192", True, "encryption-sym-aes192.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption simple S2K SHA1 AES256", True, "encryption-sym-aes256-s2k0.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted S2K SHA1 AES256", True, "encryption-sym-aes256.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted S2K SHA256 AES256", True, "encryption-sym-aes256-sha256.pgp", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption simple S2K SHA1 Twofish", True, "encryption-sym-twofish-s2k0.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted S2K SHA1 Twofish", True, "encryption-sym-twofish.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption simple Camellia128", True, "encryption-sym-camellia128-s2k0.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted Camellia128", True, "encryption-sym-camellia128.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted Camellia192", True, "encryption-sym-camellia192.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption iterated-salted Camellia256", True, "encryption-sym-camellia256.gpg", "symmetric-password.txt", "test\n")- , ("Symmetric Encryption pgcrypto", True, "encryption-sym-pgcrypto.pgp", "pgcrypto-passphrase.txt", "{\"t\":\"plus\"}")- ]--mkStrictSymmetricCase :: (String, FilePath, FilePath, BL.ByteString) -> TestTree-mkStrictSymmetricCase (label, encFile, passFile, cleartext) =- testCase label (testSymmetricEncryption encFile passFile cleartext)--strictSymmetricCases :: [(String, FilePath, FilePath, BL.ByteString)]-strictSymmetricCases =- [ ("strict policy AES128 iterated-salted S2K SEIPDv1", "encryption-sym-aes128.gpg", "symmetric-password.txt", "test\n")- , ("strict policy AES192 iterated-salted S2K SEIPDv1", "encryption-sym-aes192.gpg", "symmetric-password.txt", "test\n")- , ("strict policy AES256 iterated-salted S2K SEIPDv1", "encryption-sym-aes256.gpg", "symmetric-password.txt", "test\n")- ]--mkSyntheticNISTP256 :: B.ByteString -> (ECDSA.PublicKey, ECDSA.PrivateKey)-mkSyntheticNISTP256 bs =- let curve = ECCT.getCurveByName ECCT.SEC_p256r1- d = os2ip (B.pack [1 .. 32])- q = pointBaseMul curve d- pub = ECDSA.PublicKey curve q- priv = ECDSA.PrivateKey curve d- in (pub, priv)--syntheticNISTP256Low = mkSyntheticNISTP256 $ B.pack [1 .. 32]-syntheticNISTP256High = mkSyntheticNISTP256 $ B.pack [101 .. 132]--encryptionAndCompressionTests :: TestTree-encryptionAndCompressionTests =- testGroup- "Encryption and compression"- [ testGroup- "Compression group"- [ testCase "compressedsig.gpg" (testCompression "compressedsig.gpg")- , testCase- "compressedsig-zlib.gpg"- (testCompression "compressedsig-zlib.gpg")- , testCase- "compressedsig-bzip2.gpg"- (testCompression "compressedsig-bzip2.gpg")- , testCase- "decompressPkt classifies empty ZIP payload"- (assertEqual "empty ZIP"- (Left (EmptyCompressedPayload ZIP))- (decompressPkt (CompressedDataPkt ZIP "")))- , testCase- "decompressPkt classifies empty Uncompressed payload"- (assertEqual "empty Uncompressed"- (Left (EmptyCompressedPayload Uncompressed))- (decompressPkt (CompressedDataPkt Uncompressed "")))- , testCase- "decompressPkt classifies marker-only ZIP payload"- (let markerPkt = MarkerPkt "\x50\x47\x50"- compressed = compressPkts ZIP [markerPkt]- in assertEqual "marker-only"- (Left (MarkerOnlyPayload ZIP))- (decompressPkt compressed))- , testCase- "decompressPkt passes through non-compressed packets"- (let pkt = OtherPacketPkt 255 ""- in assertEqual "passthrough"- (Right [pkt])- (decompressPkt pkt))- ]- , testGroup- "Conduit length group"- [ testCase- "conduitCompress (ZIP)"- (testConduitOutputLength- "pubring.gpg"- (cgp DC..| conduitCompress ZIP)- 1)- , testCase- "conduitCompress (Zlib)"- (testConduitOutputLength- "pubring.gpg"- (cgp DC..| conduitCompress ZLIB)- 1)- , testCase- "conduitCompress (BZip2)"- (testConduitOutputLength- "pubring.gpg"- (cgp DC..| conduitCompress BZip2)- 1)- , testCase- "conduitToUnknownTKs"- (testConduitOutputLength "pubring.gpg" (cgp DC..| conduitToUnknownTKs) 4)- , testCase- "conduitToTKsDropping"- (testConduitOutputLength- "pubring.gpg"- (cgp DC..| conduitToTKsDropping)- 4)- , testCase- "conduitToSomeTKsDropping"- (testConduitOutputLength- "pubring.gpg"- (cgp DC..| conduitToSomeTKsDropping)- 4)- , testCase- "conduitToTKsEither reports parse failures"- testConduitToTKsEitherReportsParseFailure- , testCase- "conduitToSomeTKsEither reports parse failures"- testConduitToSomeTKsEitherReportsParseFailure- , testCase- "conduitToTKsDroppingEither reports parse failures"- testConduitToTKsDroppingEitherReportsParseFailure- , testCase- "conduitToSomeTKsDroppingEither reports parse failures"- testConduitToSomeTKsDroppingEitherReportsParseFailure- ]- , testGroup- "Encrypted data"- ( [ testCase- "Legacy symmetric encryption fixture shape"- testLegacyEncryptionFixtureShape- , testCase- "SEIPDv1 resync nonce/MDC roundtrip"- testSEIPDv1ResyncNonceMdcRoundTrip- ]- ++ map mkLegacySymmetricCase legacySymmetricCases- ++ map mkStrictSymmetricCase strictSymmetricCases- ++ [- testCase- "strict policy rejects SED (no MDC)"- (testStrictPolicyRejectsEncryption- "encryption-sym-cast5.gpg"- "symmetric-password.txt"- "unauthenticated SED")- , testCase- "strict policy rejects Simple S2K"- (testStrictPolicyRejectsEncryption- "encryption-sym-aes128-s2k0.gpg"- "symmetric-password.txt"- "Simple S2K specifier")- , testCase- "strict policy rejects non-encrypted packet after ESK prelude"- testRejectsNonEncryptedAfterESKPrelude- , testCase- "strict policy rejects SEIPDv2 with only misaligned ESK versions"- testRejectsMisalignedESKVersionForSEIPDv2- , testCase- "strict policy discards misaligned ESK when aligned ESK is present"- testDiscardsMisalignedESKWhenAlignedCandidateExists- , testCase- "strict policy rejects trailing data after SEIPD v2"- testTrailingDataRejectedStrictSEIPDv2- , testCase- "lenient policy reports DecryptTrailingData for trailing packet after SEIPD v2"- testTrailingDataReportedLenientSEIPDv2- , testCase- "conduitDecryptChecked reports DecryptClean for well-formed SEIPD v2"- testDecryptCleanSEIPDv2- , testCase- "conduitDecrypt matches conduitDecryptChecked (default)"- testOptionsMatchesCheckedDefaultSEIPDv2- , testCase- "conduitDecrypt matches legacy conduitDecrypt output (default)"- testOptionsMatchesLegacyDefaultOutputSEIPDv2- , testCase- "conduitDecrypt matches checked lenient trailing behavior"- testOptionsMatchesCheckedLenientTrailingSEIPDv2- ] )- , testGroup- "Encrypted secret keys"- [ testCase- "SUSSHA1 CAST5 IteratedSalted SHA1 RSA"- (testSecretKeyDecryption "simple.seckey" "pki-password.txt")- , testCase- "SUS16bit CAST5 IteratedSalted SHA1 RSA"- (testSecretKeyDecryption "16bitcksum.seckey" "pki-password.txt")- , testCase- "SUSSHA1 AES256 IteratedSalted SHA512 RSA"- (testSecretKeyDecryption "aes256-sha512.seckey" "pki-password.txt")- , testCase- "SUSSHA1 AES128 IteratedSalted SHA256 ECDSA"- (testSecretKeyDecryption- "nist_p-256_secretkey.gpg"- "pki-password.txt")- ]- , testGroup- "Encrypting secret keys"- [ testCase- "legacy secret key encryption rejects implicit SHA-1 protection"- (testLegacySecretKeyEncryptionRejected- "unencrypted.seckey"- "pki-password.txt")- , testCase- "SUSym secret key roundtrips"- testSUSymSecretKeyRoundTrip- , testCase- "v6 secret key encryption roundtrips with SUSAEAD"- testV6SecretKeyEncryptionRoundTrip- , testCase- "v4 AEAD OCB secret key compatibility roundtrip"- testV4SecretKeyAEADOCBRoundTripCompat- ]- , testGroup- "decrypt conduit stuff"- [ testCase- "conduitDecrypt supports PKESKv6 with raw session-key callback"- testConduitDecryptSEIPDv2WithPKESKv6RawSessionKey- , testCase- "conduitDecrypt rejects invalid PKESKv6 raw session-key length"- testConduitDecryptSEIPDv2RejectsWrongPKESKv6RawSessionKeyLength- , testCase- "conduitDecrypt unwraps PKESK RSA session keys in-library"- testConduitDecryptSEIPDv2WithPKESKRSAUnwrap- , testCase- "conduitDecrypt probes wildcard callback fallback for PKESKv3 RSA key-id packets"- testConduitDecryptSEIPDv2WithPKESKRSAUnwrapViaWildcardCallbackFallback- , testCase- "conduitDecrypt unwraps PKESKv3 RSA session keys via SHA1-CFB protected key loaded from file"- testConduitDecryptSEIPDv2WithPKESKRSAUnwrapFromProtectedKey- , testCase- "parsed RSA secret key decrypts PKCS#1 v1.5 payload"- testParsedRSASecretKeyPKCS15DecryptNotMessageNotRecognized- , testCase- "conduitDecrypt unwraps PKESK ECDH session keys in-library"- testConduitDecryptSEIPDv2WithPKESKECDHUnwrap- , testCase- "conduitDecrypt rejects ECDH ephemeral points with wrong curve length"- testConduitDecryptSEIPDv2RejectsECDHWrongEphemeralPointLength- , testCase- "conduitDecrypt unwraps PKESKv3 X25519 session keys in-library"- testConduitDecryptSEIPDv2WithPKESKX25519V3Unwrap- , testCase- "conduitDecrypt retries wildcard PKESKv3 keys across callback order"- testConduitDecryptSEIPDv2RetriesWildcardPKESKv3AcrossRecipientKeyOrder- , testCase- "conduitDecrypt retries wildcard PKESKv3 keys across long callback order"- testConduitDecryptSEIPDv2RetriesWildcardPKESKv3AcrossLongRecipientKeyOrder- , testCase- "conduitDecrypt retries wildcard PKESKv3 keys across long callback order for SEIPDv1"- testConduitDecryptSEIPDv1RetriesWildcardPKESKv3AcrossLongRecipientKeyOrder- , testCase- "unwrap callback decrypts wildcard PKESKv3 via candidates list"- testMkCandidateResolverDecryptsWildcardPKESKv3- , testCase- "conduitDecrypt wildcard resolver receives typed previous-failure diagnostics"- testConduitDecryptWildcardResolverProvidesTypedPreviousFailures- , testCase- "conduitDecryptWithReport surfaces wildcard resolver diagnostics"- testConduitDecryptWithReportCapturesWildcardResolverDiagnostics- , testCase- "conduitDecrypt rejects PKESKv3 X25519 ephemeral values with wrong length"- testConduitDecryptSEIPDv2RejectsPKESKX25519V3WrongEphemeralLength- , testCase- "conduitDecrypt falls back from Argon2 SKESK to PKESKv3 X25519"- testConduitDecryptSEIPDv2FallsBackFromArgon2SKESKToPKESKv3X25519- , testCase- "conduitDecrypt retries earlier Argon2 SKESKs when latest is unusable"- testConduitDecryptSEIPDv2FallsBackToEarlierArgon2SKESK- , testCase- "conduitDecrypt rejects ECDH KDF/KEK params outside RFC9580 Table 30"- testConduitDecryptSEIPDv2RejectsECDHNonTable30Params- , testCase- "conduitDecrypt allows v4 Curve25519Legacy RFC6637 accepted parameters"- testConduitDecryptSEIPDv2AllowsV4Curve25519LegacyRFC6637AcceptedParams- , testCase- "conduitDecrypt allows v4 Curve25519Legacy with truncated wrapped MPI"- testConduitDecryptSEIPDv2AllowsV4Curve25519LegacyWithTruncatedWrappedMPI- , testCase- "conduitDecrypt keeps v6 Curve25519Legacy ECDH strict"- testConduitDecryptSEIPDv2RejectsV6Curve25519LegacyNonTable30Params- , testCase- "conduitDecrypt unwraps PKESK X448 session keys in-library"- testConduitDecryptSEIPDv2WithPKESKX448Unwrap- , testCase- "conduitDecrypt rejects PKESKv6 X448 ephemeral values with wrong length"- testConduitDecryptSEIPDv2RejectsPKESKX448WrongEphemeralLength- , testCase- "encrypt-side session material encoding uses OpenPGP format"- testEncodeOpenPGPSessionMaterial- , testCase- "encrypt-side PKESK builder wraps RSA recipient session key"- testBuildPKESKPayloadForRecipientRSA- , testCase- "encrypt-side PKESKv3 builder supports RSA v4 interop"- testBuildPKESKv3PayloadForRecipientRSAInterop- , testCase- "encrypt-side PKESKv3 builder supports v6 recipients"- testBuildPKESKv3PayloadForRecipientSupportsV6Key- , testCase- "encrypt-side PKESKv3 builder supports RFC6637 ECDH recipients"- testBuildPKESKv3PayloadForRecipientECDHInterop- , testCase- "PKESKv3 ECDH serializer uses RFC 6637 wire format"- testPKESKv3ECDHWireFormat- , testCase- "encrypt-side PKESKv3 builder supports RFC6637 Curve25519Legacy recipients"- testBuildPKESKv3PayloadForRecipientCurve25519LegacyInterop- , testCase- "encrypt-side PKESKv3 builder rejects non-RFC6637 Curve448Legacy recipients"- testBuildPKESKv3PayloadForRecipientRejectsCurve448Legacy- , testCase- "RFC 9580 v6 X25519 PKESK uses raw-key semantics, not v3-encoded material"- testBuildPKESKv6PayloadForRecipientX25519RawKeySemantics- , testCase- "RFC 9580 v6 X448 PKESK uses raw-key semantics, not v3-encoded material"- testBuildPKESKv6PayloadForRecipientX448RawKeySemantics- , testCase- "encrypt-side policy builder emits PKESK3 for ForceV3Interop"- testBuildPKESKPayloadForRecipientWithPolicyForceV3Interop- , testCase- "encrypt-side profile helper honors recipient capability hints"- testRecipientVersionStrategyForProfileHonorsHints- , testCase- "encryptForRecipients auto-detects mixed recipient PKESK strategies"- testEncryptRecipientsWithRequestAutoDetectsMixedRecipientStrategies- , testCase- "encryptForRecipients negotiates symmetric algorithm from recipient capabilities when enabled"- testEncryptRecipientsNegotiatesSymmetricAlgorithmWhenEnabled- , testCase- "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- "recipientCapabilitiesFromSubpacketPayloads extracts preferred AEAD algorithms"- testRecipientCapabilitiesFromSubpacketPayloadsExtractsPreferredAEADAlgorithms- , testCase- "encryptForRecipients negotiates AEAD algorithm from recipient capabilities when enabled"- testEncryptRecipientsNegotiatesAEADAlgorithmWhenEnabled- , testCase- "encryptForRecipients capability negotiation fails when recipients share no AEAD algorithm"- testEncryptRecipientsNegotiationFailsWithoutCommonAEADAlgorithm- , testCase- "encryptForRecipients falls back to SEIPDv1 when recipients do not advertise SEIPDv2 support"- testEncryptRecipientsSEIPDv2FallsBackWhenRecipientsDoNotAdvertiseV2- , testCase- "encryptForRecipients rejects SEIPDv1 when recipients do not advertise MDC support"- testEncryptRecipientsRejectsSEIPDv1WhenRecipientsDoNotAdvertiseMDC- , testCase- "recipientEncryptionTargetsFromTK returns only encryption-capable candidates"- testRecipientEncryptionTargetsFromTKIncludesOnlyEncryptionCapableKeys- , testCase- "recipientEncryptionTargetFromTK prefers encryption-capable subkeys"- testRecipientEncryptionTargetFromTKPrefersSubkeyOverPrimary- , testCase- "recipientEncryptionTargetFromTK rejects TKs without encryptable key material"- testRecipientEncryptionTargetFromTKRejectsTKWithoutEncryptableKeys- , testCase- "recipientEncryptionTargetsReportFromTKAtTimestamp reports sign-only subkey rejection reasons"- testRecipientEncryptionTargetsReportFromTKAtTimestampExplainsRejections- , testCase- "recipientEncryptionTargetsReportFromTKAtTimestamp reports unsupported algorithm rejections"- testRecipientEncryptionTargetsReportFromTKAtTimestampExplainsUnsupportedAlgorithms- , testCase- "recipientEncryptionTargetsReportFromTKAtTimestamp rejects expired subkeys"- testRecipientEncryptionTargetsReportFromTKAtTimestampRejectsExpiredSubkey- , testCase- "recipientEncryptionTargetsReportFromTKAtTimestamp rejects revoked subkeys"- testRecipientEncryptionTargetsReportFromTKAtTimestampRejectsRevokedSubkey- , testCase- "recipientEncryptionTargetsFromTKAtTimestamp extracts effective self-signature capabilities"- testRecipientEncryptionTargetsFromTKAtTimestampExtractsSelfSigCapabilities- , testCase- "recipientEncryptionTargetFromTKAtTimestamp filters subkey self-signatures by timestamp"- testRecipientEncryptionTargetFromTKAtTimestampAppliesTimestampFiltering- , testCase- "recipientEncryptionTargetFromTKAtTimestampWithPolicy can prefer primary key"- testRecipientEncryptionTargetFromTKAtTimestampWithPolicyPrefersPrimary- , testCase- "recipientEncryptionTargetFromTKAtTimestampWithPolicy can prefer newest key"- testRecipientEncryptionTargetFromTKAtTimestampWithPolicyPrefersNewest- , testCase- "recipientEncryptionTargetsFromTK excludes subkeys with non-encrypt key flags"- testRecipientEncryptionTargetsFromTKRejectsKeyWithNonEncryptFlags- , testCase- "recipientEncryptionTargetsFromTK includes subkeys with no key flags advertised"- testRecipientEncryptionTargetsFromTKIncludesKeyWithNoFlags- , testCase- "encryptForRecipients optionally emits one-pass signature packets"- testEncryptRecipientsWithRequestEmitsOnePassSignatures- , testCase- "encryptForRecipients one-pass mode requires issuer metadata"- testEncryptRecipientsWithRequestRejectsOnePassWhenIssuerMetadataMissing- , testCase- "encryptForRecipients with EncryptInteropLegacy produces SEIPDv1 and PKESKv3"- testEncryptInteropLegacyProducesSEIPDv1- , testCase- "encrypt-side PKESK builder reports unsupported recipient algorithms"- testBuildPKESKPayloadUnsupportedRecipient- , testCase- "encrypt-side ECDH PKESK builder rejects SHA1 KDF policy"- testBuildPKESKPayloadRejectsECDHSHA1- , testCase- "encrypt-side canonicalize PKESK recipient id helper"- testCanonicalizePKESKRecipientIdHelper- , testCase- "conduitDecrypt decrypts seipdv2 fixture with matching v6 secret key"- testConduitDecryptSEIPDv2FixtureWithMatchingV6SecretKey- , testCase- "seipdv2-for-v4-key fixture parses as PKESKv6+SEIPDv2"- testSEIPDv2ForV4KeyArmor- , testCase- "conduitDecrypt decrypts seipdv2-for-v4-key fixture with matching v4 secret key"- testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKey- , testCase- "conduitDecrypt tries earlier PKESKs when latest is unusable"- testConduitDecryptSEIPDv2FixtureIgnoresUnusableLatestPKESK- , testCase- "conduitDecrypt matches valid recipient-id forms without caller retries"- testConduitDecryptSEIPDv2FixtureAcceptsRecipientIdWithoutCallerPermutations- , testCase- "conduitDecrypt fails PKESK exhaustion without manual session-key prompt"- testConduitDecryptPKESKFailureDoesNotRequireManualSessionMaterial- , testCase- "seipdv2-two-recipients fixture parses as PKESKv6*2+SEIPDv2"- testSEIPDv2TwoRecipientsArmor- , testCase- "seipdv2-three-recipients fixture parses as PKESKv6*3+SEIPDv2"- testSEIPDv2ThreeRecipientsArmor- , testCase- "conduitDecrypt decrypts seipdv2-two-recipients fixture with matching v4 secret key"- testConduitDecryptSEIPDv2TwoRecipientsFixtureWithMatchingV4SecretKey- , testCase- "conduitDecrypt decrypts reordered+bogus seipdv2-two-recipients PKESKs"- testConduitDecryptSEIPDv2TwoRecipientsFixtureWithReorderedAndUnusablePKESKs- , testCase- "conduitDecrypt decrypts seipdv2-three-recipients fixture with matching v4 secret key"- testConduitDecryptSEIPDv2ThreeRecipientsFixtureWithMatchingV4SecretKey- , testCase- "conduitDecrypt decrypts reordered+bogus seipdv2-three-recipients PKESKs"- testConduitDecryptSEIPDv2ThreeRecipientsFixtureWithReorderedAndUnusablePKESKs- ]- , testGroup "ESK/SKESK/ArgonS2K"- [ testCase- "encoded session key rejects too-short payloads"- testDecodeOpenPGPEncodedSessionKeyRejectsTooShort- , testCase- "encoded session key rejects algorithm/key-length mismatches"- testDecodeOpenPGPEncodedSessionKeyRejectsLengthMismatch- , testCase- "encoded session key rejects checksum mismatches"- testDecodeOpenPGPEncodedSessionKeyRejectsChecksumMismatch- , testCase "unknown SKESK version is rejected" testSKESKRejectsUnknownVersion- , testCase- "v4 SKESK encrypted session keys reject Simple S2K"- testSKESK4EncryptedSessionKeyRejectsSimpleS2K- , testCase- "v4 SKESK Argon2 encrypted ESK roundtrip across AES-128/192/256"- testSKESK4Argon2EncryptedSessionKeyRoundTripAcrossAES- , testCase- "v4 SKESK Argon2 encrypted ESK rejects legacy checksum trailer"- testSKESK4Argon2EncryptedSessionKeyRejectsTrailingChecksum- , testCase- "passphrase SKESK policy ForceV4Interop emits SKESKv4 + SEIPDv1"- testEncryptPassphraseWithPolicyForceV4Interop- , testCase- "passphrase SKESK policy PreferV6 emits SKESKv6 + SEIPDv2"- testEncryptPassphraseWithPolicyPreferV6- , testCase "Argon2 S2K derivation vector" testArgon2S2KVector- , testCase "Argon2 S2K Ord instance is total" testArgon2S2KOrdTotal- ]-- ]--testCompression :: FilePath -> Assertion-testCompression fpr = do- bs <- BL.readFile $ "tests/data/" ++ fpr- let firstpass = fmap (concatMap (either (const []) id . decompressPkt) . unBlock) . runGet get $ bs- case firstpass of- Left _ -> assertFailure $ "First pass failed on " ++ fpr- Right [] ->- assertFailure $ "First pass of " ++ fpr ++ " decoded to nothing."- Right packs -> do- let roundtrip = runPut $ put . Block $ [compressPkts ZIP packs]- let secondpass =- fmap (concatMap (either (const []) id . decompressPkt) . unBlock) . runGet get $ roundtrip- if secondpass == Right []- then assertFailure $ "Second pass of " ++ fpr ++ " decoded to nothing."- else assertEqual ("for " ++ fpr) firstpass secondpass--counter :: Monad m => DC.ConduitT a DC.Void m Int-counter = CL.fold (const . (1 +)) 0--testConduitOutputLength ::- FilePath- -> DC.ConduitT B.ByteString b (ResourceT IO) ()- -> Int- -> Assertion-testConduitOutputLength fpr c target = do- len <-- DC.runConduitRes $- CB.sourceFile ("tests/data/" ++ fpr) DC..| c DC..| counter- assertEqual ("expected length " ++ show target) target len--testConduitToTKsEitherReportsParseFailure :: Assertion-testConduitToTKsEitherReportsParseFailure = do- results <-- DC.runConduitRes $- CB.sourceFile "tests/data/uncompressed-ops-rsa.gpg" DC..|- conduitGet get DC..| conduitToTKsEither DC..| CL.consume- assertBool- "conduitToTKsEither should report parse failures for non-key packet streams"- (any (either (const True) (const False)) results)--testConduitToTKsDroppingEitherReportsParseFailure :: Assertion-testConduitToTKsDroppingEitherReportsParseFailure = do- results <-- DC.runConduitRes $- CB.sourceFile "tests/data/uncompressed-ops-rsa.gpg" DC..|- conduitGet get DC..| conduitToTKsDroppingEither DC..| CL.consume- assertBool- "conduitToTKsDroppingEither should report parse failures for non-key packet streams"- (any (either (const True) (const False)) results)--testConduitToSomeTKsEitherReportsParseFailure :: Assertion-testConduitToSomeTKsEitherReportsParseFailure = do- results <-- DC.runConduitRes $- CB.sourceFile "tests/data/uncompressed-ops-rsa.gpg" DC..|- conduitGet get DC..| conduitToSomeTKsEither DC..| CL.consume- assertBool- "conduitToSomeTKsEither should report parse failures for non-key packet streams"- (any (either (const True) (const False)) results)--testConduitToSomeTKsDroppingEitherReportsParseFailure :: Assertion-testConduitToSomeTKsDroppingEitherReportsParseFailure = do- results <-- DC.runConduitRes $- CB.sourceFile "tests/data/uncompressed-ops-rsa.gpg" DC..|- conduitGet get DC..| conduitToSomeTKsDroppingEither DC..| CL.consume- assertBool- "conduitToSomeTKsDroppingEither should report parse failures for non-key packet streams"- (any (either (const True) (const False)) results)---- This needs a lot of work--- | Decrypt a symmetric-encrypted fixture using 'defaultDecryptPolicy' and--- assert the packet structure is SKESK + SEIPDv1 and the cleartext matches.--- Only suitable for AES-128/192/256 fixtures with iterated-salted S2K.-testSymmetricEncryption :: FilePath -> FilePath -> BL.ByteString -> Assertion-testSymmetricEncryption encfile passfile cleartext = do- passphrase <- readFixtureLazy passfile- pt <- readFixturePackets encfile- assertEqual "wrong number of packets" 2 (length pt)- skesk <-- case pt of- [] -> assertFailure ("expected SKESK packet in " ++ encfile ++ " but packet list was empty") >> fail "empty fixture packet list"- (firstPkt:_) ->- case (fromPktEither firstPkt :: Either String (SKESK 'SKESKV4)) of- Left err -> assertFailure ("failed to coerce first packet to SKESK: " ++ err) >> fail err- Right x -> pure x- assertEqual "first packet should be SKESK" SKESKType (packetType skesk)- case last pt of- SymEncIntegrityProtectedDataPkt (SEIPD1 1 _) -> pure ()- other ->- assertFailure- ("second packet should be SEIPDv1 (tag 18, version 1), got: " ++ show other)- decrypted <-- catch- (DC.runConduitRes $- CL.sourceList pt DC..| conduitDecrypt (fakeCallback passphrase) DC..|- CL.consume)- (\e -> do- let err = show (e :: SomeException)- assertFailure ("decryption threw exception: " ++ err))- payload <-- case decrypted of- [] ->- assertFailure ("decryption produced no packets for " ++ encfile) >>- fail "expected decrypted literal packet"- (firstDecrypted:_) ->- case (fromPktEither firstDecrypted :: Either String LiteralData) of- Left err ->- assertFailure ("failed to coerce decrypted packet to literal data: " ++ err) >>- fail err- Right x -> pure (_literalDataPayload x)- assertEqual ("cleartext for " ++ encfile) cleartext payload- where- fakeCallback :: BL.ByteString -> String -> IO BL.ByteString- fakeCallback = const . return--testSEIPDv1ResyncNonceMdcRoundTrip :: Assertion-testSEIPDv1ResyncNonceMdcRoundTrip = do- let sa = AES256- keyBytes = B.pack [0..31]- iv = IV (B.pack [32..47])- cleartext = B.pack [1..80]- cleartextWithMDC = cleartext <> mdcTrailerForSEIPDv1 iv cleartext- ciphertext <-- either- (\e -> assertFailure ("encryptOpenPGPCfbRaw failed: " ++ show e) >> pure mempty)- pure- (encryptOpenPGPCfbRaw OpenPGPCFBResyncW sa iv cleartextWithMDC keyBytes)- (nonce, decrypted) <-- either- (\e -> assertFailure ("decryptOpenPGPCfbWithNonce failed: " ++ show e) >> pure (mempty, mempty))- pure- (decryptOpenPGPCfbWithNonce sa ciphertext keyBytes)- assertEqual- "SEIPDv1 nonce should match IV||IV[-2:]"- (seipdv1NonceFromIV iv)- nonce- assertEqual- "decrypted bytes should include payload+MDC"- cleartextWithMDC- decrypted- payloadOut <-- either- (\err -> assertFailure ("validateSEIPD1MDC failed: " ++ err) >> pure mempty)- pure- (validateSEIPD1MDC nonce decrypted)- assertEqual "MDC-verified payload should round-trip" cleartext payloadOut---- | Like 'testSymmetricEncryption' but uses 'lenientDecryptPolicy' to permit--- legacy RFC4880/RFC2440 message formats (SED, deprecated S2K, old ciphers).--- The 'Bool' indicates whether the fixture uses SEIPDv1 (True) or SED (False).-testLegacySymmetricEncryption :: Bool -> FilePath -> FilePath -> BL.ByteString -> Assertion-testLegacySymmetricEncryption expectSEIPDv1 encfile passfile cleartext = do- passphrase <- readFixtureLazy passfile- pt <- readFixturePackets encfile- assertEqual "wrong number of packets" 2 (length pt)- skesk <-- case pt of- [] -> assertFailure ("expected SKESK packet in " ++ encfile ++ " but packet list was empty") >> fail "empty fixture packet list"- (firstPkt:_) ->- case (fromPktEither firstPkt :: Either String (SKESK 'SKESKV4)) of- Left err -> assertFailure ("failed to coerce first packet to SKESK: " ++ err) >> fail err- Right x -> pure x- assertEqual "first packet should be SKESK" SKESKType (packetType skesk)- case last pt of- SymEncDataPkt {}- | not expectSEIPDv1 -> pure ()- | otherwise ->- assertFailure "expected SEIPDv1 packet (tag 18) but got SED (tag 9)"- SymEncIntegrityProtectedDataPkt (SEIPD1 1 _)- | expectSEIPDv1 -> pure ()- | otherwise ->- assertFailure "expected SED packet (tag 9) but got SEIPDv1 (tag 18)"- other -> assertFailure ("unexpected second packet: " ++ show other)- decrypted <-- catch- (DC.runConduitRes $- CL.sourceList pt DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy (\_ -> pure Nothing) (fakeCallback passphrase) DC..|- CL.consume)- (\e -> do- let err = show (e :: SomeException)- assertFailure ("decryption threw exception: " ++ err))- payload <-- case decrypted of- [] ->- assertFailure ("decryption produced no packets for " ++ encfile) >>- fail "expected decrypted literal packet"- (firstDecrypted:_) ->- case (fromPktEither firstDecrypted :: Either String LiteralData) of- Left err ->- assertFailure ("failed to coerce decrypted packet to literal data: " ++ err) >>- fail err- Right x -> pure (_literalDataPayload x)- assertEqual ("cleartext for " ++ encfile) cleartext payload- where- fakeCallback :: BL.ByteString -> String -> IO BL.ByteString- fakeCallback = const . return---- | Assert that decrypting with the strict 'defaultDecryptPolicy' throws an--- exception whose message contains the given substring.-testStrictPolicyRejectsEncryption :: FilePath -> FilePath -> String -> Assertion-testStrictPolicyRejectsEncryption encfile passfile expectedFragment = do- passphrase <- readFixtureLazy passfile- pt <- readFixturePackets encfile- result <-- try- (DC.runConduitRes $- CL.sourceList pt DC..|- conduitDecrypt (fakeCallback passphrase) DC..|- CL.consume) :: IO (Either SomeException [Pkt])- case result of- Right _ ->- assertFailure $- "Expected strict policy to reject '" ++ encfile ++- "' with error containing '" ++ expectedFragment ++ "', but decryption succeeded"- Left err ->- assertBool- ("Expected error containing '" ++ expectedFragment ++- "', got: " ++ show err)- (expectedFragment `isInfixOf` show err)- where- fakeCallback :: BL.ByteString -> String -> IO BL.ByteString- fakeCallback = const . return---- | Decrypt a file and check the 'DecryptOutcome'.-testDecryptOutcome :: FilePath -> FilePath -> DecryptOutcome -> Assertion-testDecryptOutcome encfile passfile expectedOutcome = do- passphrase <- readFixtureLazy passfile- pt <- readFixturePackets encfile- (outcome, _pkts) <-- catch- (DC.runConduitRes $- CL.sourceList pt DC..|- fuseBoth (conduitDecryptChecked (fakeCallback passphrase)) CL.consume)- (\e -> do- let err = show (e :: SomeException)- assertFailure ("decryption threw unexpected exception: " ++ err)- fail "unreachable")- assertEqual- ("DecryptOutcome for " ++ encfile)- expectedOutcome- outcome- where- fakeCallback :: BL.ByteString -> String -> IO BL.ByteString- fakeCallback = const . return---- | Build a synthetic packet list with an appended trailing packet, decrypt--- with lenient policy, and verify 'DecryptTrailingData' is reported.-testTrailingDataReportedLenient :: FilePath -> FilePath -> Assertion-testTrailingDataReportedLenient encfile passfile = do- passphrase <- readFixtureLazy passfile- pt <- readFixturePackets encfile- let trailingPkt = OtherPacketPkt 0xFE ""- ptWithTrailing = pt ++ [trailingPkt]- (outcome, _pkts) <-- catch- (DC.runConduitRes $- CL.sourceList ptWithTrailing DC..|- fuseBoth (conduitDecryptCheckedWithDecryptPolicy lenientDecryptPolicy (\_ -> pure Nothing) (fakeCallback passphrase)) CL.consume)- (\e -> do- let err = show (e :: SomeException)- assertFailure ("lenient decrypt threw unexpected exception: " ++ err)- fail "unreachable")- assertEqual- ("DecryptTrailingData for " ++ encfile)- DecryptTrailingData- outcome- where- fakeCallback :: BL.ByteString -> String -> IO BL.ByteString- fakeCallback = const . return---- | Same as above but with strict policy: should throw "after message--- integrity boundary".-testTrailingDataRejectedStrict :: FilePath -> FilePath -> Assertion-testTrailingDataRejectedStrict encfile passfile = do- passphrase <- readFixtureLazy passfile- pt <- readFixturePackets encfile- let trailingPkt = OtherPacketPkt 0xFE ""- ptWithTrailing = pt ++ [trailingPkt]- result <-- try- (DC.runConduitRes $- CL.sourceList ptWithTrailing DC..|- fuseBoth (conduitDecryptChecked (fakeCallback passphrase)) CL.consume- ) :: IO (Either SomeException (DecryptOutcome, [Pkt]))- case result of- Left err ->- assertFailure ("Expected DecryptMalformedStructure but got exception: " ++ show err)- Right (DecryptMalformedStructure malformedReason, _) ->- assertBool- ("Expected 'after message integrity boundary', got: " ++ malformedReason)- ("after message integrity boundary" `isInfixOf` malformedReason)- Right (other, _) ->- assertFailure ("Expected DecryptMalformedStructure but got: " ++ show other)- where- fakeCallback :: BL.ByteString -> String -> IO BL.ByteString- fakeCallback = const . return--encryptedSEIPDv2Packets :: IO [Pkt]-encryptedSEIPDv2Packets = do- let passphrase = mkPassphrase (BL.pack (map (fromIntegral . fromEnum) ("test" :: String)))- payload = mkClearPayload (BL.pack (map (fromIntegral . fromEnum) ("hello" :: String)))- let result = encryptMessageDefault DoNotExposeSessionMaterial passphrase payload- case result of- Left err -> fail ("encryptedSEIPDv2Packets: encryptMessageDefault failed: " ++ show err)- Right (encPayload, _) ->- DC.runConduitRes $- CB.sourceLbs (encryptedPayloadBytes encPayload) DC..| conduitGet get DC..|- CL.consume--defaultOptionsForPassphrase :: BL.ByteString -> DecryptOptions-defaultOptionsForPassphrase passphrase =- DecryptOptions- { decryptOptionsKeyResolution = DecryptWithoutPKESK- , decryptOptionsPolicy = defaultDecryptPolicy- , decryptOptionsPassphraseCallback = const (pure passphrase)- }--testDecryptCleanSEIPDv2 :: Assertion-testDecryptCleanSEIPDv2 = do- pt <- encryptedSEIPDv2Packets- let passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))- cb = const (pure passphrase)- (outcome, _) <-- catch- (DC.runConduitRes $- CL.sourceList pt DC..| fuseBoth (conduitDecryptChecked cb) CL.consume)- (\e -> assertFailure ("unexpected exception: " ++ show (e :: SomeException)) >> fail "unreachable")- assertEqual "clean SEIPD v2 should yield DecryptClean" DecryptClean outcome--testOptionsMatchesCheckedDefaultSEIPDv2 :: Assertion-testOptionsMatchesCheckedDefaultSEIPDv2 = do- pt <- encryptedSEIPDv2Packets- let passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))- cb = const (pure passphrase)- opts = defaultOptionsForPassphrase passphrase- checkedResult <-- DC.runConduitRes $- CL.sourceList pt DC..| fuseBoth (conduitDecryptChecked cb) CL.consume- optionsResult <-- DC.runConduitRes $- CL.sourceList pt DC..| fuseBoth (DCD.conduitDecrypt opts) CL.consume- assertEqual "options conduit should match checked conduit on clean decrypt" checkedResult optionsResult--testOptionsMatchesLegacyDefaultOutputSEIPDv2 :: Assertion-testOptionsMatchesLegacyDefaultOutputSEIPDv2 = do- pt <- encryptedSEIPDv2Packets- let passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))- cb = const (pure passphrase)- opts = defaultOptionsForPassphrase passphrase- legacyOutput <-- DC.runConduitRes $- CL.sourceList pt DC..| conduitDecrypt cb DC..| CL.consume- (optionsOutcome, optionsOutput) <-- DC.runConduitRes $- CL.sourceList pt DC..| fuseBoth (DCD.conduitDecrypt opts) CL.consume- assertEqual- "options conduit should report DecryptClean for clean stream"- DecryptClean- optionsOutcome- assertEqual "legacy conduit output should match options conduit output" legacyOutput optionsOutput--testTrailingDataRejectedStrictSEIPDv2 :: Assertion-testTrailingDataRejectedStrictSEIPDv2 = do- pt <- encryptedSEIPDv2Packets- let ptWithTrailing = pt ++ [OtherPacketPkt 0xFE ""]- passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))- cb = const (pure passphrase)- result <-- try- (DC.runConduitRes $- CL.sourceList ptWithTrailing DC..| fuseBoth (conduitDecryptChecked cb) CL.consume- ) :: IO (Either SomeException (DecryptOutcome, [Pkt]))- case result of- Left err ->- assertFailure ("Expected DecryptMalformedStructure but got exception: " ++ show err)- Right (DecryptMalformedStructure reason, _) ->- assertBool- ("Expected 'after message integrity boundary', got: " ++ reason)- ("after message integrity boundary" `isInfixOf` reason)- Right (other, _) ->- assertFailure ("Expected strict policy to reject trailing data but got: " ++ show other)--testTrailingDataReportedLenientSEIPDv2 :: Assertion-testTrailingDataReportedLenientSEIPDv2 = do- pt <- encryptedSEIPDv2Packets- let ptWithTrailing = pt ++ [OtherPacketPkt 0xFE ""]- passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))- cb = const (pure passphrase)- (outcome, _) <-- catch- (DC.runConduitRes $- CL.sourceList ptWithTrailing DC..|- fuseBoth (conduitDecryptCheckedWithDecryptPolicy lenientDecryptPolicy (\_ -> pure Nothing) cb) CL.consume)- (\e -> assertFailure ("unexpected exception: " ++ show (e :: SomeException)) >> fail "unreachable")- assertEqual "trailing packet should yield DecryptTrailingData" DecryptTrailingData outcome--testOptionsMatchesCheckedLenientTrailingSEIPDv2 :: Assertion-testOptionsMatchesCheckedLenientTrailingSEIPDv2 = do- pt <- encryptedSEIPDv2Packets- let ptWithTrailing = pt ++ [OtherPacketPkt 0xFE ""]- passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))- cb = const (pure passphrase)- opts =- DecryptOptions- { decryptOptionsKeyResolution = DecryptWithoutPKESK- , decryptOptionsPolicy = lenientDecryptPolicy- , decryptOptionsPassphraseCallback = cb- }- checkedResult <-- DC.runConduitRes $- CL.sourceList ptWithTrailing DC..|- fuseBoth (conduitDecryptCheckedWithDecryptPolicy lenientDecryptPolicy (\_ -> pure Nothing) cb) CL.consume- optionsResult <-- DC.runConduitRes $- CL.sourceList ptWithTrailing DC..|- fuseBoth (DCD.conduitDecrypt opts) CL.consume- assertEqual "options conduit should match checked lenient conduit with trailing data" checkedResult optionsResult--testRejectsNonEncryptedAfterESKPrelude :: Assertion-testRejectsNonEncryptedAfterESKPrelude = do- pt <- encryptedSEIPDv2Packets- let malformed =- case pt of- [] -> []- esk:_ ->- [ esk- , LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) "not-encrypted"- ]- passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))- cb = const (pure passphrase)- result <-- try- (DC.runConduitRes $- CL.sourceList malformed DC..| fuseBoth (conduitDecryptChecked cb) CL.consume- ) :: IO (Either SomeException (DecryptOutcome, [Pkt]))- case result of- Left err ->- assertFailure ("Expected DecryptMalformedStructure but got exception: " ++ show err)- Right (DecryptMalformedStructure reason, _) ->- assertBool- ("Expected ESK prelude shape failure, got: " ++ reason)- ("ESK packets must immediately precede encrypted data" `isInfixOf` reason)- Right (other, _) ->- assertFailure ("Expected DecryptMalformedStructure but got: " ++ show other)--testRejectsMisalignedESKVersionForSEIPDv2 :: Assertion-testRejectsMisalignedESKVersionForSEIPDv2 = do- pt <- encryptedSEIPDv2Packets- let malformed =- case pt of- (SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 sa _ s2k _ _ _)):rest) ->- SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k Nothing)) : rest- _ -> pt- passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))- cb = const (pure passphrase)- result <-- try- (DC.runConduitRes $- CL.sourceList malformed DC..| fuseBoth (conduitDecryptChecked cb) CL.consume- ) :: IO (Either SomeException (DecryptOutcome, [Pkt]))- case result of- Left err ->- assertFailure ("Expected DecryptMalformedStructure but got exception: " ++ show err)- Right (DecryptMalformedStructure reason, _) ->- assertBool- ("Expected payload/ESK alignment failure, got: " ++ reason)- ("version-aligned" `isInfixOf` reason)- Right (other, _) ->- assertFailure ("Expected DecryptMalformedStructure but got: " ++ show other)--testDiscardsMisalignedESKWhenAlignedCandidateExists :: Assertion-testDiscardsMisalignedESKWhenAlignedCandidateExists = do- pt <- encryptedSEIPDv2Packets- let withMixedPrelude =- case pt of- (SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 sa aa s2k iv esk tag)):rest) ->- SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k Nothing)) : SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 sa aa s2k iv esk tag)) : rest- _ -> pt- passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))- cb = const (pure passphrase)- (outcome, _) <-- catch- (DC.runConduitRes $- CL.sourceList withMixedPrelude DC..| fuseBoth (conduitDecryptChecked cb) CL.consume)- (\e -> assertFailure ("unexpected exception: " ++ show (e :: SomeException)) >> fail "unreachable")- assertEqual- "misaligned ESKs should be discarded when an aligned ESK is present"- DecryptClean- outcome--testSecretKeyDecryption :: FilePath -> FilePath -> Assertion-testSecretKeyDecryption keyfile passfile = do- passphrase <- readFixtureLazy passfile- kr <-- DC.runConduitRes $- CB.sourceFile (fixturePath keyfile) DC..| conduitGet get DC..|- CL.consume- SecretKey pkp ska <-- case kr of- [] ->- assertFailure ("no packets found in secret key fixture " ++ keyfile) >>- fail "empty secret key fixture"- (firstPkt:_) ->- case (fromPktEither firstPkt :: Either String SecretKey) of- Left err -> assertFailure ("failed to coerce key packet to SecretKey: " ++ err) >> fail err- Right x -> pure x- decrypted <-- case decryptPrivateKey (pkp, ska) passphrase of- Left err ->- assertFailure ("secret key decryption failed: " ++ err) >>- fail "secret key decryption failed"- Right x -> pure x- case decrypted of- SUUnencrypted skey _ -> doPkeyAndSkeyMatch (_pubkey pkp) skey- other ->- assertFailure- ("secret key decryption should produce an unencrypted secret key, got: " ++- show other)--testLegacySecretKeyEncryptionRejected :: FilePath -> FilePath -> Assertion-testLegacySecretKeyEncryptionRejected keyfile passfile = do- passphrase <- readFixtureLazy passfile- kr <-- DC.runConduitRes $- CB.sourceFile (fixturePath keyfile) DC..| conduitGet get DC..|- CL.consume- SecretKey pkp ska <-- case kr of- [] ->- assertFailure ("no packets found in secret key fixture " ++ keyfile) >>- fail "empty secret key fixture"- (firstPkt:_) ->- case (fromPktEither firstPkt :: Either String SecretKey) of- Left err -> assertFailure ("failed to coerce key packet to SecretKey: " ++ err) >> fail err- Right x -> pure x- case- encryptPrivateKeyWithPolicyAndSaltAndIV- defaultPolicy- pkp- (Salt "\226~\197\a\202#\"G")- (IV "\187\219\253I\236\204\t5D\196\NAK>;\202\185\t")- ska- passphrase of- Left err | "explicit legacy override required" `isInfixOf` err -> pure ()- Left err ->- assertFailure- ("legacy secret key encryption should be rejected with a policy error, got: " ++- err)- Right _ ->- assertFailure "legacy secret key encryption should reject implicit SHA-1 protection"--testV6SecretKeyEncryptionRoundTrip :: Assertion-testV6SecretKeyEncryptionRoundTrip = do- passphrase <- readPKIPassphrase- armors <- loadArmor "v6-secret.pgp.aa"- armor <-- case armors of- (a:_) -> pure a- [] ->- assertFailure "v6-secret.pgp.aa should contain one armored payload" >>- fail "expected one armored payload"- let packets = parsePkts (armorPayload armor)- SecretKey pkp ska <-- case [sk | pkt <- packets, Right sk <- [fromPktEither pkt :: Either String SecretKey]] of- (sk:_) -> pure sk- [] ->- assertFailure "v6-secret.pgp.aa should contain a secret key packet" >>- fail "expected secret key packet"- originalSKey <-- case ska of- SUUnencrypted skey _ -> pure skey- _ ->- assertFailure "v6-secret.pgp.aa should contain unencrypted secret key material" >>- fail "expected unencrypted secret key"- changed <-- case- encryptPrivateKeyWithPolicyAndSaltAndIV- defaultPolicy- pkp- (Salt "1234567890ABCDEF")- (IV "1234567890ABCDE")- ska- passphrase of- Left err ->- assertFailure ("v6 secret key encryption failed: " ++ err) >>- fail "v6 secret key encryption failed"- Right x -> pure x- case changed of- SUSAEAD AES256 OCB (Argon2 _ t p em) _ encryptedPayload -> do- assertEqual- "v6 secret key encryption should use expected Argon2 t parameter"- 1- t- assertEqual- "v6 secret key encryption should use expected Argon2 p parameter"- 4- p- assertEqual- "v6 secret key encryption should use expected Argon2 encoded memory parameter"- 15- em- assertBool- "v6 secret key encryption should emit encrypted payload"- (not (BL.null encryptedPayload))- _ ->- assertFailure "v6 secret key encryption should emit SUSAEAD/AES256/OCB with Argon2 S2K"- let serialized = runPut (put (toPkt (SecretKey pkp changed)))- reparsed = parsePkts serialized- (parsedPKP, parsedSKA) <-- case reparsed of- (SecretKeyPkt pkpayload skaddendum:_) -> pure (pkpayload, skaddendum)- _ ->- assertFailure "re-serialized v6 secret key should parse back as a secret key packet" >>- fail "expected serialized secret key packet"- decrypted <-- case decryptPrivateKey (parsedPKP, parsedSKA) passphrase of- Left err ->- assertFailure ("v6 secret key roundtrip decryption failed: " ++ err) >>- fail "v6 secret key roundtrip decryption failed"- Right x -> pure x- case decrypted of- SUUnencrypted skey _ ->- assertEqual- "v6 secret key roundtrip should preserve secret key material"- originalSKey- skey- _ -> assertFailure "v6 secret key roundtrip should decrypt to unencrypted secret material"--testV4SecretKeyAEADOCBRoundTripCompat :: Assertion-testV4SecretKeyAEADOCBRoundTripCompat = do- (pkp, privateKey) <- loadUnencryptedRsaSigner- let skey = RSAPrivateKey (RSA_PrivateKey privateKey)- passphrase = "legacy-aead-passphrase"- compatPolicy =- (policyForRFC RFC4880)- { policySecretKeyProtection = policySecretKeyProtection defaultPolicy- }- salt = Salt (B.pack [0x00 .. 0x0f])- iv = IV (B.pack [0x10 .. 0x1e])- encrypted <-- case- encryptPrivateKeyWithPolicyAndSaltAndIV- compatPolicy- pkp- salt- iv- (SUUnencrypted skey 0)- passphrase of- Left err ->- assertFailure ("v4 AEAD secret key encryption failed: " ++ err) >>- fail "v4 AEAD secret key encryption failed"- Right x -> pure x- case encrypted of- SUSAEAD AES256 OCB (Argon2 _ t p em) _ encryptedPayload -> do- assertEqual "v4 AEAD secret key encryption should use expected Argon2 t" 1 t- assertEqual "v4 AEAD secret key encryption should use expected Argon2 p" 4 p- assertEqual "v4 AEAD secret key encryption should use expected Argon2 encoded-memory" 15 em- assertBool- "v4 AEAD secret key encryption should emit encrypted payload"- (not (BL.null encryptedPayload))- _ ->- assertFailure "v4 secret key encryption should emit SUSAEAD/AES256/OCB with Argon2 S2K"- let serialized = runPut (put (toPkt (SecretKey pkp encrypted)))- reparsed = parsePkts serialized- (parsedPKP, parsedSKA) <-- case reparsed of- (SecretKeyPkt pkpayload skaddendum:_) -> pure (pkpayload, skaddendum)- _ ->- assertFailure "re-serialized v4 AEAD secret key should parse back as a secret key packet" >>- fail "expected serialized secret key packet"- decrypted <-- case decryptPrivateKey (parsedPKP, parsedSKA) passphrase of- Left err ->- assertFailure ("v4 AEAD secret key roundtrip decryption failed: " ++ err) >>- fail "v4 AEAD secret key roundtrip decryption failed"- Right x -> pure x- case decrypted of- SUUnencrypted skey' _ ->- assertEqual- "v4 AEAD secret key roundtrip should preserve secret key material"- skey- skey'- _ -> assertFailure "v4 AEAD secret key roundtrip should decrypt to unencrypted secret material"--testSUSymSecretKeyRoundTrip :: Assertion-testSUSymSecretKeyRoundTrip = do- passphrase <- readPKIPassphrase- packets <-- DC.runConduitRes $- CB.sourceFile "tests/data/unencrypted.seckey" DC..| conduitGet get DC..|- CL.consume- (pkp, skey) <-- case packets of- (SecretKeyPkt pkpayload (SUUnencrypted sk _):_) -> pure (pkpayload, sk)- _ ->- assertFailure "unencrypted.seckey did not begin with an unencrypted secret key packet" >>- fail "expected unencrypted secret key packet"- let cleartext = legacyRsaSecretKeyBytes skey- cleartextLBS = BL.fromStrict cleartext- clearWithChecksum = cleartextLBS <> runPut (putWord16be (legacyChecksum16 cleartext))- iv = IV (B.pack [0..15])- sa = AES128- keyMaterial <-- case string2Key (Simple DeprecatedMD5) 16 passphrase of- Left err ->- assertFailure ("failed to derive SUSym key material: " ++ renderS2KError err) >>- fail "failed to derive SUSym key material"- Right km -> pure km- encrypted <-- case encryptNoNonce sa (Simple DeprecatedMD5) iv (BL.toStrict clearWithChecksum) keyMaterial of- Left err ->- assertFailure ("failed to encrypt legacy SUSym secret key: " ++ show err) >>- fail "failed to encrypt legacy SUSym secret key"- Right bs -> pure bs- let legacySka = SUSym sa iv (BL.fromStrict encrypted)- serialized = runPut (put (toPkt (SecretKey pkp legacySka)))- reparsed = parsePkts serialized- (parsedPKP, parsedSKA) <-- case reparsed of- (SecretKeyPkt pkpayload skaddendum:_) -> pure (pkpayload, skaddendum)- _ ->- assertFailure "serialized SUSym secret key should parse back as a secret key packet" >>- fail "expected serialized secret key packet"- decrypted <-- case decryptPrivateKey (parsedPKP, parsedSKA) passphrase of- Left err ->- assertFailure ("SUSym secret key decryption failed: " ++ err) >>- fail "SUSym secret key decryption failed"- Right x -> pure x- case decrypted of- SUUnencrypted parsedSKey _ ->- assertEqual- "SUSym secret key roundtrip should preserve secret key material"- skey- parsedSKey- _ -> assertFailure "SUSym secret key roundtrip should decrypt to unencrypted secret material"--legacyRsaSecretKeyBytes :: SKey -> B.ByteString-legacyRsaSecretKeyBytes (RSAPrivateKey (RSA_PrivateKey (RSA.PrivateKey _ d p q _ _ _))) =- BL.toStrict $- runPut $- case inverse q p of- Just u -> do- put (MPI d)- put (MPI p)- put (MPI q)- put (MPI u)- Nothing -> error "invalid RSA key in legacy secret key test"-legacyRsaSecretKeyBytes _ = error "legacyRsaSecretKeyBytes requires an RSA secret key"--legacyChecksum16 :: B.ByteString -> Word16-legacyChecksum16 =- fromIntegral .- B.foldl' (\acc octet -> (acc + fromIntegral octet) `mod` (65536 :: Integer)) 0--testLegacyEncryptionFixtureShape :: Assertion-testLegacyEncryptionFixtureShape = do- packets <-- DC.runConduitRes $- CB.sourceFile "tests/data/encryption.gpg" DC..| conduitGet get DC..| CL.consume- let hasKeyEncapsulation = any isKeyEncapsulation packets- hasEncryptedData = any isEncryptedData packets- assertBool- "encryption.gpg should contain PKESK or SKESK key-encapsulation"- hasKeyEncapsulation- assertBool "encryption.gpg should contain encrypted data" hasEncryptedData- where- isKeyEncapsulation (SKESKPkt _) = True- isKeyEncapsulation (PKESKPkt _) = True- isKeyEncapsulation _ = False- isEncryptedData SymEncDataPkt {} = True- isEncryptedData SymEncIntegrityProtectedDataPkt {} = True- isEncryptedData _ = False--testBuildPKESKPayloadForRecipientRSA :: Assertion-testBuildPKESKPayloadForRecipientRSA = do- (baseRecipient, privateKey) <- loadUnencryptedRsaSigner- let recipient = setKeyVersion V6 baseRecipient- sessionKey = SessionKey (B.replicate 32 0x2e)- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk builder rsa payload"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback _ =- pure- (Just- PKESKRecipientKey- { pkeskRecipientPKPayload = Nothing- , pkeskRecipientSKey = RSAPrivateKey (RSA_PrivateKey privateKey)- })- sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey- pkeskPayloadResult <- buildPKESKPayloadForRecipient PreferV6 recipient sessionMaterial- pkeskPayload <-- case pkeskPayloadResult of- Left err ->- assertFailure ("buildPKESKPayloadForRecipient failed: " ++ show err) >>- fail "buildPKESKPayloadForRecipient failed"- Right p -> pure p- case pkeskPayload of- PKESKPayloadV6Packet (PKESKPayloadV6 _ RSA eskBytesLazy) -> do- let eskBytes = BL.toStrict eskBytesLazy- assertBool "PKESKv6 RSA ESK should include MPI framing" (B.length eskBytes >= 2)- let mpiBits = fromIntegral (B.index eskBytes 0) * 256 + fromIntegral (B.index eskBytes 1)- mpiLen = (mpiBits + 7) `div` 8- assertEqual- "PKESKv6 RSA ESK should be exactly one RFC9580 MPI"- (2 + mpiLen)- (B.length eskBytes)- other ->- assertFailure ("Expected PKESKv6 RSA payload, got " ++ show other)- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList- [ PKESKPkt pkeskPayload- , SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))- ] DC..|- conduitDecryptWithPKESKContext keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "PKESK builder RSA decrypt payload" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)--testBuildPKESKv3PayloadForRecipientRSAInterop :: Assertion-testBuildPKESKv3PayloadForRecipientRSAInterop = do- (baseRecipient, privateKey) <- loadUnencryptedRsaSigner- let recipient = setKeyVersion V4 baseRecipient- sessionKey = SessionKey (B.replicate 32 0x2d)- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk builder rsa v3 interop payload"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyCallback _ = pure (Just (RSAPrivateKey (RSA_PrivateKey privateKey)))- keyContextCallback pkt = do- msk <- keyCallback pkt- pure $- fmap- (\sk ->- PKESKRecipientKey- { pkeskRecipientPKPayload = Nothing- , pkeskRecipientSKey = sk- })- msk- sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey- pkeskPayloadResult <- buildPKESKv3PayloadForRecipient recipient (pkeskV3SessionMaterial sessionMaterial)- pkeskPayload <-- case pkeskPayloadResult of- Left err ->- assertFailure ("buildPKESKv3PayloadForRecipient failed: " ++ show err) >>- fail "buildPKESKv3PayloadForRecipient failed"- Right p@(PKESKPayloadV3Packet (PKESKPayloadV3 _ _ RSA _)) -> pure p- Right other ->- assertFailure ("Expected PKESK3 RSA payload, got " ++ show other) >>- fail "Unexpected PKESK payload variant"- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList- [ PKESKPkt pkeskPayload- , SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))- ] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "PKESKv3 RSA interop decrypt payload" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)--testBuildPKESKv3PayloadForRecipientSupportsV6Key :: Assertion-testBuildPKESKv3PayloadForRecipientSupportsV6Key = do- (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner- let recipient = setKeyVersion V6 baseRecipient- sessionKey = SessionKey (B.replicate 32 0x11)- let expectedKeyId = EightOctetKeyId (BL.take 8 (unFingerprint (fingerprint recipient)))- sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey- result <- buildPKESKv3PayloadForRecipient recipient (pkeskV3SessionMaterial sessionMaterial)- case result of- Right (PKESKPayloadV3Packet (PKESKPayloadV3 _ keyId RSA _)) ->- assertEqual- "PKESKv3 builder should derive v6 key ID from the high-order 64 bits of the fingerprint"- expectedKeyId- keyId- Right payload ->- assertFailure ("Expected PKESK3 RSA payload for v6 recipient, got " ++ show payload)- Left err ->- assertFailure ("Expected PKESK3 RSA payload for v6 recipient, got error " ++ show err)--testBuildPKESKv3PayloadForRecipientECDHInterop :: Assertion-testBuildPKESKv3PayloadForRecipientECDHInterop = do- let (recipientPub, recipientPriv) = syntheticNISTP256Low- recipient =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- ECDH- (ECDHPubKey (ECDSAPubKey (ECDSA_PublicKey recipientPub)) SHA256 AES128)- sessionKey = SessionKey (B.replicate 32 0x21)- salt = Salt (B.pack [0x20 .. 0x3f])- payload = "pkesk builder ecdh v3 interop payload"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- recipientSKey = ECDHPrivateKey (ECDSA_PrivateKey recipientPriv)- passphraseCallback _ = pure BL.empty- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipient- , pkeskRecipientSKey = recipientSKey- }))- sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey- pkeskPayloadResult <- buildPKESKv3PayloadForRecipient recipient (pkeskV3SessionMaterial sessionMaterial)- pkeskPayload <-- case pkeskPayloadResult of- Left err ->- assertFailure ("buildPKESKv3PayloadForRecipient failed: " ++ show err) >>- fail "buildPKESKv3PayloadForRecipient failed"- Right p@(PKESKPayloadV3Packet (PKESKPayloadV3 _ _ ECDH _)) -> pure p- Right other ->- assertFailure ("Expected PKESK3 ECDH payload, got " ++ show other) >>- fail "Unexpected PKESK payload variant"- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList- [ PKESKPkt pkeskPayload- , SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))- ] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "PKESKv3 ECDH interop decrypt payload" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)---- | Verify that the PKESKv3 ECDH serializer writes the wrapped-key field using the--- RFC 6637 §8 wire format (MPI(ephemeral) || 1-octet-count || C) rather than the--- old double-MPI encoding that caused interoperability failures.-testPKESKv3ECDHWireFormat :: Assertion-testPKESKv3ECDHWireFormat = do- let (recipientPub, _) = syntheticNISTP256Low- recipient =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- ECDH- (ECDHPubKey (ECDSAPubKey (ECDSA_PublicKey recipientPub)) SHA256 AES256)- -- Use a session key whose first byte is 0x00 so leading-zero stripping- -- is observable if i2osp truncates the wrapped key.- sessionKey = SessionKey (B.cons 0x00 (B.replicate 31 0xAB))- sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey- pkeskResult <- buildPKESKv3PayloadForRecipient recipient (pkeskV3SessionMaterial sessionMaterial)- case pkeskResult of- Left err -> assertFailure ("buildPKESKv3PayloadForRecipient failed: " ++ show err)- Right p -> do- -- Serialise the packet to wire bytes then parse the PKESK body manually.- let wire = BL.toStrict (runPut (put (PKESKPkt p)))- -- Skip new-format tag byte (0xC1), then parse the packet length.- -- For the test we just round-trip through parsePkts and check the body layout.- case parsePkts (BL.fromStrict wire) of- [PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ _ ECDH (ephMPI NE.:| [wrappedMPI])))] -> do- -- The ephemeral MPI should be non-trivially large (EC point).- assertBool "ephemeral MPI should be non-empty" (unMPI ephMPI > 0)- -- The wrapped MPI stores the raw i2osp bytes; for AES-256 the RFC 3394- -- ciphertext is 48 bytes. i2osp strips leading zeros so the stored- -- integer may represent fewer bytes, but candidateWrappedRFC3394Ciphertexts- -- handles that. What we're checking here is the WIRE format:- -- after the ephemeral MPI there must be exactly 1-byte-len + C bytes,- -- not a second MPI header.- --- -- Re-serialise and inspect the bytes after the ephemeral MPI manually.- let ephBytes = BL.toStrict (runPut (put ephMPI))- pktBody =- -- wire: 0xC1 | varlen | version(1) | keyid(8) | pka(1) | body- -- skip tag(1) + len(variable) + version(1) + keyid(8) + pka(1)- -- We just serialise the body directly via putPKESKv3SessionKeyMaterial equivalent:- BL.toStrict (runPut (put (PKESKPkt (PKESKPayloadV3Packet- (PKESKPayloadV3 3 (EightOctetKeyId (BL.replicate 8 0)) ECDH- (ephMPI NE.:| [wrappedMPI]))))))- -- Find the position of the body after tag+len+version+keyid+pka.- -- Tag: 1 byte; new-format len: 1 or 2 bytes; version: 1; keyid: 8; pka: 1 = 12 or 13 bytes- -- Instead of parsing the header, just verify the body doesn't start with- -- the two-byte MPI bit-count of the wrapped key.- -- After ephBytes in the body, the next byte should be the 1-byte count (24-48),- -- not the high byte of an MPI bit-count (which would be 0x01 for 383-bit keys).- -- We search for ephBytes in pktBody and check what follows.- let stripped = dropPKESKv3Header pktBody- afterEph = B.drop (B.length ephBytes) stripped- assertBool- "PKESKv3 ECDH wire body: after ephemeral MPI there must be at least 1 byte for wrapped-key length"- (not (B.null afterEph))- let wrappedLenByte = fromIntegral (B.head afterEph) :: Int- assertBool- ("PKESKv3 ECDH wire: wrapped-key length byte " ++- show wrappedLenByte ++- " should be a valid RFC 3394 wrapped-key length (24, 32, 40, or 48)")- (wrappedLenByte `elem` [24, 32, 40, 48])- assertEqual- "PKESKv3 ECDH wire: bytes after length field must equal length"- wrappedLenByte- (B.length (B.tail afterEph))- other ->- assertFailure ("Expected single PKESKv3 ECDH packet after round-trip, got " ++ show other)- where- -- Drop the new-format packet header (tag byte + variable-length field +- -- version byte + 8-byte key-id + pka byte) to reach the session-key material.- dropPKESKv3Header bs- | B.length bs < 3 = bs- | otherwise =- let lenByte = B.index bs 1- headerLen- | lenByte < 192 = 2 -- 1-byte length- | lenByte < 224 = 3 -- 2-byte length- | otherwise = 6 -- 5-byte length (rare)- in B.drop (headerLen + 1 + 8 + 1) bs-testBuildPKESKv3PayloadForRecipientCurve25519LegacyInterop :: Assertion-testBuildPKESKv3PayloadForRecipientCurve25519LegacyInterop = do- let recipientSecretRaw = B.pack [1 .. 32]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize Curve25519Legacy recipient secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- recipient =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- ECDH- (ECDHPubKey- (EdDSAPubKey Ed25519 (PrefixedNativeEPoint (EPoint (os2ip (B.cons 0x40 recipientPublicRaw)))))- SHA256- AES256)- recipientSKey =- ECDHPrivateKey- (ECDSA_PrivateKey- (ECDSA.PrivateKey (ECCT.getCurveByName ECCT.SEC_p256r1) (os2ip recipientSecretRaw)))- sessionKey = SessionKey (B.replicate 32 0x22)- salt = Salt (B.pack [0x40 .. 0x5f])- payload = "pkesk builder curve25519legacy v3 interop payload"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipient- , pkeskRecipientSKey = recipientSKey- }))- sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey- pkeskPayloadResult <- buildPKESKv3PayloadForRecipient recipient (pkeskV3SessionMaterial sessionMaterial)- pkeskPayload <-- case pkeskPayloadResult of- Left err ->- assertFailure ("buildPKESKv3PayloadForRecipient failed: " ++ show err) >>- fail "buildPKESKv3PayloadForRecipient failed"- Right p@(PKESKPayloadV3Packet (PKESKPayloadV3 _ _ ECDH (ephemeralMPI NE.:| _))) -> do- let ephemeralBytes = i2osp (unMPI ephemeralMPI)- assertEqual- "PKESKv3 Curve25519Legacy ephemeral point should be 0x40-prefixed 33-octet value"- 33- (B.length ephemeralBytes)- assertEqual- "PKESKv3 Curve25519Legacy ephemeral point should use RFC6637 0x40 prefix"- 0x40- (B.head ephemeralBytes)- pure p- Right other ->- assertFailure ("Expected PKESK3 ECDH payload, got " ++ show other) >>- fail "Unexpected PKESK payload variant"- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList- [ PKESKPkt pkeskPayload- , SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))- ] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "PKESKv3 Curve25519Legacy interop decrypt payload" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)--testBuildPKESKv3PayloadForRecipientRejectsCurve448Legacy :: Assertion-testBuildPKESKv3PayloadForRecipientRejectsCurve448Legacy = do- let recipient =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- ECDH- (ECDHPubKey- (EdDSAPubKey Ed448 (PrefixedNativeEPoint (EPoint 1)))- SHA512- AES256)- sessionKey = SessionKey (B.replicate 32 0x23)- sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey- result <- buildPKESKv3PayloadForRecipient recipient (pkeskV3SessionMaterial sessionMaterial)- case result of- Left (InvalidRecipientKeyMaterial ECDH err)- | "RFC6637-compatible" `isInfixOf` err -> pure ()- | otherwise ->- assertFailure- ("Expected RFC6637 compatibility rejection, got: " ++ err)- Left err ->- assertFailure ("Expected InvalidRecipientKeyMaterial ECDH, got " ++ show err)- Right payload ->- assertFailure ("Expected RFC6637 compatibility rejection, got payload: " ++ show payload)---- | Conformance test: v6 X25519 PKESK uses raw session key, not v3-encoded material.--- RFC 9580 specifies that v6 X25519 wraps the raw session key directly (no algorithm byte or checksum).--- When the raw 32-byte key is AES-KW wrapped with RFC3394: 32 bytes + 8-byte integrity = 40 bytes.--- If v3-encoded (1 byte algo + 32 bytes key + 2 bytes checksum + ~5 bytes padding = 40 bytes unwrapped),--- the wrapped result would be ~48 bytes, so we verify wrapped size is 40.-testBuildPKESKv6PayloadForRecipientX25519RawKeySemantics :: Assertion-testBuildPKESKv6PayloadForRecipientX25519RawKeySemantics = do- let recipientSecretRaw = B.pack [1 .. 32]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize X25519 recipient secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- v4Recipient =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip recipientPublicRaw))))- recipient = setKeyVersion V6 v4Recipient- sessionKey = SessionKey (B.replicate 32 0x24)- sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey- pkeskPayloadResult <- buildPKESKPayloadForRecipient PreferV6 recipient sessionMaterial- case pkeskPayloadResult of- Left err ->- assertFailure ("buildPKESKPayloadForRecipient failed for v6 X25519: " ++ show err)- Right (PKESKPayloadV6Packet (PKESKPayloadV6 _ X25519 eskBytesLazy)) -> do- let eskBytes = BL.toStrict eskBytesLazy- assertX25519EskShape "v6 X25519 raw-key conformance" eskBytes- let wrappedLen = fromIntegral (B.index eskBytes 32) :: Int- assertEqual- "v6 X25519 wrapped session key (AES-KW RFC3394 of raw 32 bytes) must be 40 bytes"- 40- wrappedLen- Right other ->- assertFailure ("Expected PKESKPayloadV6 with X25519, got " ++ show other)---- | Conformance test: v6 X448 PKESK uses raw session key, not v3-encoded material.--- RFC 9580 specifies that v6 X448 wraps the raw session key directly (no algorithm byte or checksum).--- When the raw 32-byte key is AES-KW wrapped with RFC3394: 32 bytes + 8-byte integrity = 40 bytes.--- If v3-encoded (1 byte algo + 32 bytes key + 2 bytes checksum + ~5 bytes padding = 40 bytes unwrapped),--- the wrapped result would be ~48 bytes, so we verify wrapped size is 40.-testBuildPKESKv6PayloadForRecipientX448RawKeySemantics :: Assertion-testBuildPKESKv6PayloadForRecipientX448RawKeySemantics = do- let recipientSecretRaw = B.pack [1 .. 56]- recipientSecret =- case CE.eitherCryptoError (C448.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize X448 recipient secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C448.toPublic recipientSecret) :: B.ByteString- v4Recipient =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X448- (EdDSAPubKey Ed448 (NativeEPoint (EPoint (os2ip recipientPublicRaw))))- recipient = setKeyVersion V6 v4Recipient- sessionKey = SessionKey (B.replicate 32 0x25)- sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey- pkeskPayloadResult <- buildPKESKPayloadForRecipient PreferV6 recipient sessionMaterial- case pkeskPayloadResult of- Left err ->- assertFailure ("buildPKESKPayloadForRecipient failed for v6 X448: " ++ show err)- Right (PKESKPayloadV6Packet (PKESKPayloadV6 _ X448 eskBytesLazy)) -> do- let eskBytes = BL.toStrict eskBytesLazy- assertX448EskShape "v6 X448 raw-key conformance" eskBytes- let wrappedLen = fromIntegral (B.index eskBytes 56) :: Int- assertEqual- "v6 X448 wrapped session key (AES-KW RFC3394 of raw 32 bytes) must be 40 bytes"- 40- wrappedLen- Right other ->- assertFailure ("Expected PKESKPayloadV6 with X448, got " ++ show other)--testBuildPKESKPayloadForRecipientWithPolicyForceV3Interop :: Assertion-testBuildPKESKPayloadForRecipientWithPolicyForceV3Interop = do- (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner- let recipient = setKeyVersion V4 baseRecipient- sessionKey = SessionKey (B.replicate 32 0x16)- sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey- result <-- buildPKESKPayloadForRecipient ForceV3Interop recipient sessionMaterial- case result of- Right (PKESKPayloadV3Packet (PKESKPayloadV3 _ _ RSA _)) -> pure ()- Right payload ->- assertFailure ("Expected PKESK3 RSA for ForceV3Interop, got " ++ show payload)- Left err ->- assertFailure ("Expected PKESK3 RSA for ForceV3Interop, got error " ++ show err)--testRecipientVersionStrategyForProfileHonorsHints :: Assertion-testRecipientVersionStrategyForProfileHonorsHints = do- (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner- let recipient = setKeyVersion V6 baseRecipient- case recipientVersionStrategyForProfile EncryptStrictDefault (recipientEncryptionTargetWithStrategy recipient RecipientForceV3Interop) of- Right RecipientForceV3Interop -> pure ()- other ->- assertFailure ("Expected explicit recipient hint to win, got " ++ show other)- case recipientVersionStrategyForProfile EncryptInteropLegacy (recipientEncryptionTarget recipient) of- Right RecipientForceV3Interop -> pure ()- other ->- assertFailure ("Expected legacy profile fallback to force v3, got " ++ show other)- let v4Recipient = setKeyVersion V4 recipient- case recipientVersionStrategyForProfile EncryptStrictDefault (recipientEncryptionTarget recipient) of- Right RecipientPreferV6 -> pure ()- other ->- assertFailure ("Expected strict profile auto-detect to prefer v6 for v6 recipients, got " ++ show other)- case recipientVersionStrategyForProfile EncryptStrictDefault (recipientEncryptionTarget v4Recipient) of- Right RecipientForceV3Interop -> pure ()- other ->- assertFailure ("Expected strict profile auto-detect to force v3 for legacy recipients, got " ++ show other)--testEncryptRecipientsWithRequestAutoDetectsMixedRecipientStrategies :: Assertion-testEncryptRecipientsWithRequestAutoDetectsMixedRecipientStrategies = do- (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner- let v4Recipient = setKeyVersion V4 baseRecipient- v6Recipient = setKeyVersion V6 baseRecipient- request =- RecipientEncryptRequest- { recipientEncryptRequestTargets =- [ recipientEncryptionTarget v6Recipient- , recipientEncryptionTarget v4Recipient- ]- , recipientEncryptRequestPayloadShape = defaultRecipientPayloadShape- , recipientEncryptRequestPayload = "recipient autodetect"- , recipientEncryptRequestSymmetricOverride = Just AES256- , recipientEncryptRequestOverrides =- RecipientEncryptRequestSEIPDv2Overrides- { recipientEncryptRequestAEADOverride = Just OCB- , recipientEncryptRequestChunkSizeOverride = Just 6- , recipientEncryptRequestSaltOverride = Just (Salt (B.replicate 32 0x77))- }- }- result <- encryptForRecipients request- case result of- Left err ->- assertFailure ("Expected mixed-recipient request to encrypt successfully, got " ++ show err)- Right RecipientEncryptResult { recipientEncryptPackets = packets } -> do- let pkesks = [p | PKESKPkt p <- packets]- assertBool- "strict profile auto-detect should emit PKESKv6 for v6 recipients"- (any isPKESK6 pkesks)- assertBool- "strict profile auto-detect should emit PKESKv3 for v4 recipients"- (any isPKESK3 pkesks)- where- isPKESK6 PKESKPayloadV6Packet {} = True- isPKESK6 _ = False- isPKESK3 PKESKPayloadV3Packet {} = True- isPKESK3 _ = False--testEncryptRecipientsNegotiatesSymmetricAlgorithmWhenEnabled :: Assertion-testEncryptRecipientsNegotiatesSymmetricAlgorithmWhenEnabled = 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 = [AES128, AES256]- , recipientCapabilityPreferredAEADAlgorithms = [OCB]- }- v4Caps =- RecipientCapabilities- { recipientCapabilityKeyVersion = V4- , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient- , recipientCapabilityKeyFlags = Set.empty- , recipientCapabilityFeatures = Set.empty- , recipientCapabilityPreferredSymmetricAlgorithms = [AES128]- , recipientCapabilityPreferredAEADAlgorithms = [OCB]- }- request =- RecipientEncryptRequest- { recipientEncryptRequestTargets =- [ recipientEncryptionTargetWithCapabilities v6Recipient v6Caps- , recipientEncryptionTargetWithCapabilities v4Recipient v4Caps- ]- , recipientEncryptRequestPayloadShape = defaultRecipientPayloadShape- , recipientEncryptRequestPayload = "recipient capability negotiation payload"- , recipientEncryptRequestSymmetricOverride = Nothing- , recipientEncryptRequestOverrides =- RecipientEncryptRequestSEIPDv2Overrides- { recipientEncryptRequestAEADOverride = Just OCB- , recipientEncryptRequestChunkSizeOverride = Just 6- , recipientEncryptRequestSaltOverride = Just (Salt (B.replicate 32 0x22))- }- }- result <- encryptForRecipientsWithCapabilityNegotiation RecipientCapabilityNegotiationOn request- case result of- Left err ->- assertFailure ("Expected recipient capability negotiation to succeed, got " ++ show err)- Right RecipientEncryptResult { recipientEncryptSessionMaterial = material } ->- assertEqual- "capability negotiation should pick common preferred symmetric algorithm"- AES128- (pkeskSessionAlgorithm material)--testEncryptRecipientsNegotiatesSymmetricAlgorithmByDefault :: Assertion-testEncryptRecipientsNegotiatesSymmetricAlgorithmByDefault = 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 = [AES128, AES256]- , recipientCapabilityPreferredAEADAlgorithms = [OCB]- }- v4Caps =- RecipientCapabilities- { recipientCapabilityKeyVersion = V4- , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient- , recipientCapabilityKeyFlags = Set.empty- , recipientCapabilityFeatures = Set.empty- , recipientCapabilityPreferredSymmetricAlgorithms = [AES128]- , recipientCapabilityPreferredAEADAlgorithms = [OCB]- }- request =- RecipientEncryptRequest- { recipientEncryptRequestTargets =- [ recipientEncryptionTargetWithCapabilities v6Recipient v6Caps- , recipientEncryptionTargetWithCapabilities v4Recipient v4Caps- ]- , recipientEncryptRequestPayloadShape = defaultRecipientPayloadShape- , recipientEncryptRequestPayload = "default recipient capability negotiation payload"- , recipientEncryptRequestSymmetricOverride = Nothing- , recipientEncryptRequestOverrides =- RecipientEncryptRequestSEIPDv2Overrides- { recipientEncryptRequestAEADOverride = Just OCB- , recipientEncryptRequestChunkSizeOverride = Just 6- , recipientEncryptRequestSaltOverride = Just (Salt (B.replicate 32 0x24))- }- }- result <- encryptForRecipients request- case result of- Left err ->- assertFailure ("Expected default encryptForRecipients negotiation to succeed, got " ++ show err)- Right RecipientEncryptResult { recipientEncryptSessionMaterial = material } ->- assertEqual- "encryptForRecipients should negotiate recipient capabilities by default"- 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 = [AES128, AES256]- , recipientCapabilityPreferredAEADAlgorithms = [OCB]- }- v4Caps =- RecipientCapabilities- { recipientCapabilityKeyVersion = V4- , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient- , recipientCapabilityKeyFlags = Set.empty- , recipientCapabilityFeatures = Set.empty- , recipientCapabilityPreferredSymmetricAlgorithms = [AES128]- , recipientCapabilityPreferredAEADAlgorithms = [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- (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 = [AES128]- , recipientCapabilityPreferredAEADAlgorithms = [OCB]- }- v4Caps =- RecipientCapabilities- { recipientCapabilityKeyVersion = V4- , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient- , recipientCapabilityKeyFlags = Set.empty- , recipientCapabilityFeatures = Set.empty- , recipientCapabilityPreferredSymmetricAlgorithms = [AES256]- , recipientCapabilityPreferredAEADAlgorithms = [OCB]- }- request =- RecipientEncryptRequest- { recipientEncryptRequestTargets =- [ recipientEncryptionTargetWithCapabilities v6Recipient v6Caps- , recipientEncryptionTargetWithCapabilities v4Recipient v4Caps- ]- , recipientEncryptRequestPayloadShape = defaultRecipientPayloadShape- , recipientEncryptRequestPayload = "recipient capability mismatch payload"- , recipientEncryptRequestSymmetricOverride = Nothing- , recipientEncryptRequestOverrides =- RecipientEncryptRequestSEIPDv2Overrides- { recipientEncryptRequestAEADOverride = Just OCB- , recipientEncryptRequestChunkSizeOverride = Just 6- , recipientEncryptRequestSaltOverride = Just (Salt (B.replicate 32 0x23))- }- }- result <- encryptForRecipientsWithCapabilityNegotiation RecipientCapabilityNegotiationOn request- case result of- Left (RecipientCapabilitySelectionFailure (RecipientCapabilityNoCommonSymmetricAlgorithms _)) -> pure ()- Left other ->- assertFailure ("Expected RecipientCapabilityNoCommonSymmetricAlgorithms, got " ++ show other)- Right _ ->- assertFailure "Expected recipient capability negotiation to fail without common symmetric algorithm"--testRecipientCapabilitiesFromSubpacketPayloadsExtractsPreferredAEADAlgorithms :: Assertion-testRecipientCapabilitiesFromSubpacketPayloadsExtractsPreferredAEADAlgorithms = do- (recipient, _privateKey) <- loadUnencryptedRsaSigner- let caps =- recipientCapabilitiesFromSubpacketPayloads- recipient- [OtherSigSub 39 (BL.pack [9, 2, 7, 3, 9, 2])]- assertEqual- "preferred AEAD ciphersuites subpacket should extract unique AEAD preferences in declaration order"- [OCB, GCM]- (recipientCapabilityPreferredAEADAlgorithms caps)--testEncryptRecipientsNegotiatesAEADAlgorithmWhenEnabled :: Assertion-testEncryptRecipientsNegotiatesAEADAlgorithmWhenEnabled = 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.fromList [FeatureSEIPDv1, FeatureSEIPDv2]- , recipientCapabilityPreferredSymmetricAlgorithms = [AES128, AES256]- , recipientCapabilityPreferredAEADAlgorithms = [GCM, OCB]- }- v4Caps =- RecipientCapabilities- { recipientCapabilityKeyVersion = V4- , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient- , recipientCapabilityKeyFlags = Set.empty- , recipientCapabilityFeatures = Set.fromList [FeatureSEIPDv1, FeatureSEIPDv2]- , recipientCapabilityPreferredSymmetricAlgorithms = [AES128]- , recipientCapabilityPreferredAEADAlgorithms = [GCM]- }- request =- RecipientEncryptRequest- { recipientEncryptRequestTargets =- [ recipientEncryptionTargetWithCapabilities v6Recipient v6Caps- , recipientEncryptionTargetWithCapabilities v4Recipient v4Caps- ]- , recipientEncryptRequestPayloadShape = defaultRecipientPayloadShape- , recipientEncryptRequestPayload = "recipient AEAD capability negotiation payload"- , recipientEncryptRequestSymmetricOverride = Nothing- , recipientEncryptRequestOverrides =- RecipientEncryptRequestSEIPDv2Overrides- { recipientEncryptRequestAEADOverride = Nothing- , recipientEncryptRequestChunkSizeOverride = Just 6- , recipientEncryptRequestSaltOverride = Just (Salt (B.replicate 32 0x26))- }- }- result <- encryptForRecipientsWithCapabilityNegotiation RecipientCapabilityNegotiationOn request- case result of- Left err ->- assertFailure ("Expected AEAD capability negotiation to succeed, got " ++ show err)- Right RecipientEncryptResult { recipientEncryptPackets = packets- , recipientEncryptSessionMaterial = material- } -> do- assertEqual- "AEAD capability negotiation should preserve symmetric negotiation"- AES128- (pkeskSessionAlgorithm material)- case [aa | SymEncIntegrityProtectedDataPkt (SEIPD2 _ aa _ _ _) <- packets] of- [selectedAEAD] ->- assertEqual- "AEAD capability negotiation should pick common preferred AEAD algorithm"- GCM- selectedAEAD- other ->- assertFailure ("Expected one SEIPDv2 packet, got " ++ show other)--testEncryptRecipientsNegotiationFailsWithoutCommonAEADAlgorithm :: Assertion-testEncryptRecipientsNegotiationFailsWithoutCommonAEADAlgorithm = 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.fromList [FeatureSEIPDv1, FeatureSEIPDv2]- , recipientCapabilityPreferredSymmetricAlgorithms = [AES256]- , recipientCapabilityPreferredAEADAlgorithms = [OCB]- }- v4Caps =- RecipientCapabilities- { recipientCapabilityKeyVersion = V4- , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient- , recipientCapabilityKeyFlags = Set.empty- , recipientCapabilityFeatures = Set.fromList [FeatureSEIPDv1, FeatureSEIPDv2]- , recipientCapabilityPreferredSymmetricAlgorithms = [AES256]- , recipientCapabilityPreferredAEADAlgorithms = [GCM]- }- request =- RecipientEncryptRequest- { recipientEncryptRequestTargets =- [ recipientEncryptionTargetWithCapabilities v6Recipient v6Caps- , recipientEncryptionTargetWithCapabilities v4Recipient v4Caps- ]- , recipientEncryptRequestPayloadShape = defaultRecipientPayloadShape- , recipientEncryptRequestPayload = "recipient AEAD mismatch payload"- , recipientEncryptRequestSymmetricOverride = Just AES256- , recipientEncryptRequestOverrides =- RecipientEncryptRequestSEIPDv2Overrides- { recipientEncryptRequestAEADOverride = Nothing- , recipientEncryptRequestChunkSizeOverride = Just 6- , recipientEncryptRequestSaltOverride = Just (Salt (B.replicate 32 0x27))- }- }- result <- encryptForRecipientsWithCapabilityNegotiation RecipientCapabilityNegotiationOn request- case result of- Left (RecipientCapabilitySelectionFailure (RecipientCapabilityNoCommonAEADAlgorithms _)) -> pure ()- Left other ->- assertFailure ("Expected RecipientCapabilityNoCommonAEADAlgorithms, got " ++ show other)- Right _ ->- assertFailure "Expected recipient capability negotiation to fail without common AEAD algorithm"--testEncryptRecipientsSEIPDv2FallsBackWhenRecipientsDoNotAdvertiseV2 :: Assertion-testEncryptRecipientsSEIPDv2FallsBackWhenRecipientsDoNotAdvertiseV2 = do- (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner- let v4Recipient = setKeyVersion V4 baseRecipient- v6Recipient = setKeyVersion V6 baseRecipient- sharedCaps keyVersion recipient =- RecipientCapabilities- { recipientCapabilityKeyVersion = keyVersion- , recipientCapabilityPublicKeyAlgorithm = _pkalgo recipient- , recipientCapabilityKeyFlags = Set.empty- , recipientCapabilityFeatures = Set.singleton FeatureSEIPDv1- , recipientCapabilityPreferredSymmetricAlgorithms = [AES256]- , recipientCapabilityPreferredAEADAlgorithms = [OCB]- }- request =- RecipientEncryptRequest- { recipientEncryptRequestTargets =- [ recipientEncryptionTargetWithCapabilities v6Recipient (sharedCaps V6 v6Recipient)- , recipientEncryptionTargetWithCapabilities v4Recipient (sharedCaps V4 v4Recipient)- ]- , recipientEncryptRequestPayloadShape = defaultRecipientPayloadShape- , recipientEncryptRequestPayload = "recipient seipd fallback payload"- , recipientEncryptRequestSymmetricOverride = Just AES256- , recipientEncryptRequestOverrides =- RecipientEncryptRequestSEIPDv2Overrides- { recipientEncryptRequestAEADOverride = Nothing- , recipientEncryptRequestChunkSizeOverride = Just 6- , recipientEncryptRequestSaltOverride = Just (Salt (B.replicate 32 0x28))- }- }- result <- encryptForRecipients request- case result of- Left err ->- assertFailure ("Expected fallback to SEIPDv1 when recipients do not advertise SEIPDv2, got " ++ show err)- Right RecipientEncryptResult { recipientEncryptPackets = packets } -> do- let seipdPkts = [p | SymEncIntegrityProtectedDataPkt p <- packets]- case seipdPkts of- [SEIPD1 1 _] -> pure ()- [SEIPD2 {}] -> assertFailure "Expected SEIPDv1 fallback when recipients do not advertise SEIPDv2 support"- other -> assertFailure ("Unexpected SEIPD packets: " ++ show other)--testEncryptRecipientsRejectsSEIPDv1WhenRecipientsDoNotAdvertiseMDC :: Assertion-testEncryptRecipientsRejectsSEIPDv1WhenRecipientsDoNotAdvertiseMDC = do- (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner- let v4Recipient = setKeyVersion V4 baseRecipient- caps =- RecipientCapabilities- { recipientCapabilityKeyVersion = V4- , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient- , recipientCapabilityKeyFlags = Set.empty- , recipientCapabilityFeatures = Set.singleton FeatureSEIPDv2- , recipientCapabilityPreferredSymmetricAlgorithms = [AES256]- , recipientCapabilityPreferredAEADAlgorithms = [OCB]- }- request =- RecipientEncryptRequest- { recipientEncryptRequestTargets =- [recipientEncryptionTargetWithCapabilities v4Recipient caps]- , recipientEncryptRequestPayloadShape = defaultRecipientPayloadShape- , recipientEncryptRequestPayload = "recipient seipd v1 capability failure payload"- , recipientEncryptRequestSymmetricOverride = Just AES256- , recipientEncryptRequestOverrides =- RecipientEncryptRequestSEIPDv1Overrides- { recipientEncryptRequestIVOverride = Nothing- }- }- result <- encryptForRecipients request- case result of- Left (RecipientCapabilitySelectionFailure (RecipientCapabilityMissingSEIPDv1Support _)) -> pure ()- Left other ->- assertFailure ("Expected RecipientCapabilityMissingSEIPDv1Support, got " ++ show other)- Right _ ->- assertFailure "Expected SEIPDv1 encryption to fail when recipients do not advertise MDC support"--testRecipientEncryptionTargetsFromTKIncludesOnlyEncryptionCapableKeys :: Assertion-testRecipientEncryptionTargetsFromTKIncludesOnlyEncryptionCapableKeys = do- (signingPrimary, _signingKey) <- loadDeterministicEd25519Signer- (baseEncryptingSubkey, _privateKey) <- loadUnencryptedRsaSigner- let tkUnknown =- TKUnknown- { _tkuKey = (signingPrimary, Nothing)- , _tkuRevs = []- , _tkuUIDs = []- , _tkuUAts = []- , _tkuSubs =- [ ( PublicSubkeyPkt (setKeyTimestamp (_timestamp signingPrimary) baseEncryptingSubkey)- , []- )- ]- }- tk =- case fromUnknownToTK tkUnknown of- Right (SomePublicTK publicTk) -> publicTk- Right (SomeSecretTK _) -> error "expected public TK for recipient target test"- Left err -> error err- encryptingSubkey = setKeyTimestamp (_timestamp signingPrimary) baseEncryptingSubkey- targets = recipientEncryptionTargetsFromTK tk- assertEqual "TKUnknown-derived targets should include only encryption-capable keys" 1 (length targets)- case targets of- [target] ->- assertEqual- "TKUnknown-derived target should preserve selected encryption key"- (fingerprint encryptingSubkey)- (fingerprint (recipientEncryptionTargetKey target))- _ ->- assertFailure "Expected exactly one encryption-capable target"--testRecipientEncryptionTargetFromTKPrefersSubkeyOverPrimary :: Assertion-testRecipientEncryptionTargetFromTKPrefersSubkeyOverPrimary = do- (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner- let primary = setKeyVersion V4 baseRecipient- subkey = setKeyVersion V6 baseRecipient- tkUnknown =- TKUnknown- { _tkuKey = (primary, Nothing)- , _tkuRevs = []- , _tkuUIDs = []- , _tkuUAts = []- , _tkuSubs = [(PublicSubkeyPkt subkey, [])]- }- tk =- case fromUnknownToTK tkUnknown of- Right (SomePublicTK publicTk) -> publicTk- Right (SomeSecretTK _) -> error "expected public TK for recipient target selection test"- Left err -> error err- case recipientEncryptionTargetFromTK tk of- Right target ->- assertEqual- "TKUnknown-derived single target should prioritize encryption subkeys"- (fingerprint subkey)- (fingerprint (recipientEncryptionTargetKey target))- Left err ->- assertFailure ("Expected TKUnknown-derived target selection to succeed, got " ++ show err)--testRecipientEncryptionTargetFromTKRejectsTKWithoutEncryptableKeys :: Assertion-testRecipientEncryptionTargetFromTKRejectsTKWithoutEncryptableKeys = do- (signingPrimary, _signingKey) <- loadDeterministicEd25519Signer- let tkUnknown =- TKUnknown- { _tkuKey = (signingPrimary, Nothing)- , _tkuRevs = []- , _tkuUIDs = []- , _tkuUAts = []- , _tkuSubs = []- }- tk =- case fromUnknownToTK tkUnknown of- Right (SomePublicTK publicTk) -> publicTk- Right (SomeSecretTK _) -> error "expected public TK for recipient rejection test"- Left err -> error err- case recipientEncryptionTargetFromTK tk of- Left RecipientCapabilityNoEncryptableKeyMaterialInTK -> pure ()- Left other ->- assertFailure ("Expected RecipientCapabilityNoEncryptableKeyMaterialInTK, got " ++ show other)- Right _ ->- assertFailure "Expected TKUnknown-derived target selection to reject non-encryptable TKs"--testRecipientEncryptionTargetsReportFromTKAtTimestampExplainsRejections :: Assertion-testRecipientEncryptionTargetsReportFromTKAtTimestampExplainsRejections = do- (baseKey, signingKey) <- loadUnencryptedRsaSigner- let signatureTime = ThirtyTwoBitTimeStamp 1700000000- primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey- subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000001) baseKey- subkeyBindingSig <-- signSubkeyBindingWithRSAExtrasAt- primary- subkey- signingKey- signatureTime- [SigSubPacket False (KeyFlags (Set.fromList [SignDataKey]))]- let tk =- TKUnknown- { _tkuKey = (primary, Nothing)- , _tkuRevs = []- , _tkuUIDs = []- , _tkuUAts = []- , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]- }- report = recipientEncryptionTargetsReportFromTKAtTimestamp signatureTime tk- acceptedFps =- map (fingerprint . recipientEncryptionTargetKey) (recipientEncryptionTargetsAccepted report)- rejectedReasons =- map recipientEncryptionTargetRejectedReason (recipientEncryptionTargetsRejected report)- assertBool- "report should keep encryption-eligible primary key as accepted"- (fingerprint primary `elem` acceptedFps)- assertEqual- "report should explain sign-only subkey rejection"- [RecipientTargetMissingEncryptionFlags subkey (Set.fromList [SignDataKey])]- rejectedReasons--testRecipientEncryptionTargetsReportFromTKAtTimestampExplainsUnsupportedAlgorithms :: Assertion-testRecipientEncryptionTargetsReportFromTKAtTimestampExplainsUnsupportedAlgorithms = do- (signingPrimary, _signingKey) <- loadDeterministicEd25519Signer- let tk =- TKUnknown- { _tkuKey = (signingPrimary, Nothing)- , _tkuRevs = []- , _tkuUIDs = []- , _tkuUAts = []- , _tkuSubs = []- }- report =- recipientEncryptionTargetsReportFromTKAtTimestamp- (_timestamp signingPrimary)- tk- assertEqual- "unsupported key algorithms should be surfaced in report rejections"- [RecipientTargetUnsupportedAlgorithm (_pkalgo signingPrimary)]- (map recipientEncryptionTargetRejectedReason (recipientEncryptionTargetsRejected report))--testRecipientEncryptionTargetsReportFromTKAtTimestampRejectsExpiredSubkey :: Assertion-testRecipientEncryptionTargetsReportFromTKAtTimestampRejectsExpiredSubkey = do- (baseKey, signingKey) <- loadUnencryptedRsaSigner- let signatureTime = ThirtyTwoBitTimeStamp 1700000000- targetTime = ThirtyTwoBitTimeStamp 1700000020- primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey- subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000001) baseKey- subkeyBindingSig <-- signSubkeyBindingWithRSAExtrasAt- primary- subkey- signingKey- signatureTime- [ SigSubPacket False (KeyFlags (Set.fromList [EncryptCommunicationsKey]))- , SigSubPacket False (KeyExpirationTime (ThirtyTwoBitDuration 5))- ]- let tk =- TKUnknown- { _tkuKey = (primary, Nothing)- , _tkuRevs = []- , _tkuUIDs = []- , _tkuUAts = []- , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]- }- report = recipientEncryptionTargetsReportFromTKAtTimestamp targetTime tk- rejectedReasons = map recipientEncryptionTargetRejectedReason (recipientEncryptionTargetsRejected report)- assertBool- "expired subkey should be rejected from recipient targets"- (RecipientTargetNotValidAtTimestamp subkey targetTime `elem` rejectedReasons)--testRecipientEncryptionTargetsReportFromTKAtTimestampRejectsRevokedSubkey :: Assertion-testRecipientEncryptionTargetsReportFromTKAtTimestampRejectsRevokedSubkey = do- (baseKey, signingKey) <- loadUnencryptedRsaSigner- let bindingTime = ThirtyTwoBitTimeStamp 1700000000- revocationTime = ThirtyTwoBitTimeStamp 1700000005- targetTime = ThirtyTwoBitTimeStamp 1700000010- primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey- subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000001) baseKey- subkeyBindingSig <-- signSubkeyBindingWithRSAExtrasAt- primary- subkey- signingKey- bindingTime- [SigSubPacket False (KeyFlags (Set.fromList [EncryptCommunicationsKey]))]- subkeyRevocationSig <-- signSubkeyRevocationWithRSAAt- primary- subkey- signingKey- revocationTime- let tk =- TKUnknown- { _tkuKey = (primary, Nothing)- , _tkuRevs = []- , _tkuUIDs = []- , _tkuUAts = []- , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig, subkeyRevocationSig])]- }- report = recipientEncryptionTargetsReportFromTKAtTimestamp targetTime tk- rejectedReasons = map recipientEncryptionTargetRejectedReason (recipientEncryptionTargetsRejected report)- assertBool- "revoked subkey should be rejected from recipient targets"- (RecipientTargetRevoked subkey `elem` rejectedReasons)--testRecipientEncryptionTargetsFromTKAtTimestampExtractsSelfSigCapabilities :: Assertion-testRecipientEncryptionTargetsFromTKAtTimestampExtractsSelfSigCapabilities = do- (primary, signingKey) <- loadUnencryptedRsaSigner- (subkey, _privateKey) <- loadUnencryptedRsaSigner- let signatureTime = ThirtyTwoBitTimeStamp 1700000000- directKeySig <-- signDirectKeyWithRSAExtrasAt- primary- signingKey- signatureTime- [ SigSubPacket False (PreferredSymmetricAlgorithms [AES128])- , SigSubPacket False (OtherSigSub 39 (BL.pack [9, 2, 7, 3]))- ]- subkeyBindingSig <-- signSubkeyBindingWithRSAExtrasAt- primary- subkey- signingKey- signatureTime- [SigSubPacket False (KeyFlags (Set.fromList [EncryptCommunicationsKey]))]- let tk =- TKUnknown- { _tkuKey = (primary, Nothing)- , _tkuRevs = [directKeySig]- , _tkuUIDs = []- , _tkuUAts = []- , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]- }- targets = recipientEncryptionTargetsFromTKAtTimestamp signatureTime tk- case targets of- (target:_) ->- case recipientEncryptionTargetCapabilities target of- Nothing ->- assertFailure "Expected TKUnknown-derived target to include extracted capabilities"- Just caps -> do- assertEqual- "self-signature capability extraction should include primary-key symmetric preferences"- [AES128]- (recipientCapabilityPreferredSymmetricAlgorithms caps)- assertEqual- "self-signature capability extraction should include primary-key preferred AEAD algorithms"- [OCB, GCM]- (recipientCapabilityPreferredAEADAlgorithms caps)- assertEqual- "self-signature capability extraction should include subkey key flags"- (Set.fromList [EncryptCommunicationsKey])- (recipientCapabilityKeyFlags caps)- [] ->- assertFailure "Expected at least one TKUnknown-derived target"--testRecipientEncryptionTargetFromTKAtTimestampAppliesTimestampFiltering :: Assertion-testRecipientEncryptionTargetFromTKAtTimestampAppliesTimestampFiltering = do- (primary, signingKey) <- loadUnencryptedRsaSigner- (subkey, _privateKey) <- loadUnencryptedRsaSigner- let signatureTime = ThirtyTwoBitTimeStamp 1700000100- beforeSignature = ThirtyTwoBitTimeStamp 1699999999- subkeyBindingSig <-- signSubkeyBindingWithRSAExtrasAt- primary- subkey- signingKey- signatureTime- [SigSubPacket False (KeyFlags (Set.fromList [EncryptCommunicationsKey]))]- let tk =- TKUnknown- { _tkuKey = (primary, Nothing)- , _tkuRevs = []- , _tkuUIDs = []- , _tkuUAts = []- , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]- }- case recipientEncryptionTargetFromTKAtTimestamp beforeSignature tk of- Left err ->- assertFailure ("Expected timestamp-scoped TKUnknown 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"- Just caps ->- assertEqual- "subkey binding created after target timestamp should not contribute key flags"- Set.empty- (recipientCapabilityKeyFlags caps)--testRecipientEncryptionTargetFromTKAtTimestampWithPolicyPrefersPrimary :: Assertion-testRecipientEncryptionTargetFromTKAtTimestampWithPolicyPrefersPrimary = do- (baseKey, signingKey) <- loadUnencryptedRsaSigner- let signatureTime = ThirtyTwoBitTimeStamp 1700000000- primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey- subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000001) baseKey- subkeyBindingSig <-- signSubkeyBindingWithRSAExtrasAt- primary- subkey- signingKey- signatureTime- [SigSubPacket False (KeyFlags (Set.fromList [EncryptCommunicationsKey]))]- let tk =- TKUnknown- { _tkuKey = (primary, Nothing)- , _tkuRevs = []- , _tkuUIDs = []- , _tkuUAts = []- , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]- }- case recipientEncryptionTargetFromTKAtTimestampWithPolicy RecipientTargetSelectionPreferPrimary signatureTime tk of- Left err ->- assertFailure ("Expected policy-based recipient selection to succeed, got " ++ show err)- Right target ->- assertEqual- "prefer-primary policy should select primary key when both primary and subkey are valid"- (fingerprint primary)- (fingerprint (recipientEncryptionTargetKey target))--testRecipientEncryptionTargetFromTKAtTimestampWithPolicyPrefersNewest :: Assertion-testRecipientEncryptionTargetFromTKAtTimestampWithPolicyPrefersNewest = do- (baseKey, signingKey) <- loadUnencryptedRsaSigner- let signatureTime = ThirtyTwoBitTimeStamp 1700000000- primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey- subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000002) baseKey- subkeyBindingSig <-- signSubkeyBindingWithRSAExtrasAt- primary- subkey- signingKey- signatureTime- [SigSubPacket False (KeyFlags (Set.fromList [EncryptCommunicationsKey]))]- let tk =- TKUnknown- { _tkuKey = (primary, Nothing)- , _tkuRevs = []- , _tkuUIDs = []- , _tkuUAts = []- , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]- }- case recipientEncryptionTargetFromTKAtTimestampWithPolicy RecipientTargetSelectionPreferNewestCreationTime signatureTime tk of- Left err ->- assertFailure ("Expected policy-based recipient selection to succeed, got " ++ show err)- Right target ->- assertEqual- "prefer-newest policy should select newest valid key"- (fingerprint subkey)- (fingerprint (recipientEncryptionTargetKey target))--testRecipientEncryptionTargetsFromTKRejectsKeyWithNonEncryptFlags :: Assertion-testRecipientEncryptionTargetsFromTKRejectsKeyWithNonEncryptFlags = do- (baseKey, signingKey) <- loadUnencryptedRsaSigner- let signatureTime = ThirtyTwoBitTimeStamp 1700000000- primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey- subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000001) baseKey- subkeyBindingSig <-- signSubkeyBindingWithRSAExtrasAt- primary- subkey- signingKey- signatureTime- [SigSubPacket False (KeyFlags (Set.fromList [SignDataKey]))]- let tk =- TKUnknown- { _tkuKey = (primary, Nothing)- , _tkuRevs = []- , _tkuUIDs = []- , _tkuUAts = []- , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]- }- targets = recipientEncryptionTargetsFromTKAtTimestamp signatureTime tk- let subkeyFp = fingerprint subkey- primaryFp = fingerprint primary- targetFps = map (fingerprint . recipientEncryptionTargetKey) targets- assertBool- "subkey with sign-only key flags should be excluded from encryption targets"- (subkeyFp `notElem` targetFps)- assertBool- "primary key (no explicit flags) should still be included as encryption target"- (primaryFp `elem` targetFps)--testRecipientEncryptionTargetsFromTKIncludesKeyWithNoFlags :: Assertion-testRecipientEncryptionTargetsFromTKIncludesKeyWithNoFlags = do- (baseKey, signingKey) <- loadUnencryptedRsaSigner- let signatureTime = ThirtyTwoBitTimeStamp 1700000000- primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey- subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000001) baseKey- subkeyBindingSig <-- signSubkeyBindingWithRSAExtrasAt- primary- subkey- signingKey- signatureTime- []- let tk =- TKUnknown- { _tkuKey = (primary, Nothing)- , _tkuRevs = []- , _tkuUIDs = []- , _tkuUAts = []- , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]- }- targets = recipientEncryptionTargetsFromTKAtTimestamp signatureTime tk- let subkeyFp = fingerprint subkey- targetFps = map (fingerprint . recipientEncryptionTargetKey) targets- assertBool- "subkey with no key flags should be included as encryption target (legacy-compatible)"- (subkeyFp `elem` targetFps)--testEncryptRecipientsWithRequestEmitsOnePassSignatures :: Assertion-testEncryptRecipientsWithRequestEmitsOnePassSignatures = do- (baseRecipient, privateKey) <- loadUnencryptedRsaSigner- let recipient = setKeyVersion V6 baseRecipient- signature =- SigV4- BinarySig- RSA- SHA256- []- [SigSubPacket False (Issuer (EightOctetKeyId (BL.pack [0x01 .. 0x08])))]- 0- (MPI 1 :| [])- payload = "recipient one-pass signature payload"- request =- RecipientEncryptRequest- { recipientEncryptRequestTargets = [recipientEncryptionTarget recipient]- , recipientEncryptRequestPayloadShape =- defaultRecipientPayloadShape- { recipientPayloadUseOnePassSignatures = True- , recipientPayloadSignatures = [signature]- }- , recipientEncryptRequestPayload = payload- , recipientEncryptRequestSymmetricOverride = Just AES256- , recipientEncryptRequestOverrides =- RecipientEncryptRequestSEIPDv2Overrides- { recipientEncryptRequestAEADOverride = Just OCB- , recipientEncryptRequestChunkSizeOverride = Just 6- , recipientEncryptRequestSaltOverride = Just (Salt (B.replicate 32 0x55))- }- }- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipient- , pkeskRecipientSKey = RSAPrivateKey (RSA_PrivateKey privateKey)- }))- passphraseCallback _ = pure BL.empty- result <- encryptForRecipients request- packets <-- case result of- Left err ->- assertFailure ("Expected one-pass encryption request to succeed, got " ++ show err) >>- fail "encryptForRecipients failed"- Right RecipientEncryptResult { recipientEncryptPackets = encryptedPackets } -> pure encryptedPackets- decrypted <-- DC.runConduitRes $- CL.sourceList packets DC..|- conduitDecryptWithPKESKContext keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [OnePassSignaturePkt (OPSPayloadV3Packet (OPSPayloadV3 3 BinarySig SHA256 RSA _ False)), LiteralDataPkt _ _ _ gotPayload, SignaturePkt _] ->- assertEqual "decrypted payload matches original plaintext" (BL.fromStrict payload) gotPayload- other ->- assertFailure ("Expected decrypted [OPS3, LiteralData, Signature], got " ++ show other)--testEncryptRecipientsWithRequestRejectsOnePassWhenIssuerMetadataMissing :: Assertion-testEncryptRecipientsWithRequestRejectsOnePassWhenIssuerMetadataMissing = do- (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner- let recipient = setKeyVersion V6 baseRecipient- signature = SigV4 BinarySig RSA SHA256 [] [] 0 (MPI 1 :| [])- request =- RecipientEncryptRequest- { recipientEncryptRequestTargets = [recipientEncryptionTarget recipient]- , recipientEncryptRequestPayloadShape =- defaultRecipientPayloadShape- { recipientPayloadUseOnePassSignatures = True- , recipientPayloadSignatures = [signature]- }- , recipientEncryptRequestPayload = "recipient one-pass missing issuer metadata payload"- , recipientEncryptRequestSymmetricOverride = Just AES256- , recipientEncryptRequestOverrides =- RecipientEncryptRequestSEIPDv2Overrides- { recipientEncryptRequestAEADOverride = Just OCB- , recipientEncryptRequestChunkSizeOverride = Just 6- , recipientEncryptRequestSaltOverride = Just (Salt (B.replicate 32 0x56))- }- }- result <- encryptForRecipients request- case result of- Left (PayloadBuildFailure err) ->- assertBool- "missing issuer metadata should surface as payload build failure"- ("without issuer metadata" `isInfixOf` err)- Left err ->- assertFailure ("Expected PayloadBuildFailure for missing OPS issuer metadata, got " ++ show err)- Right _ ->- assertFailure "Expected one-pass request without issuer metadata to fail"--testEncryptInteropLegacyProducesSEIPDv1 :: Assertion-testEncryptInteropLegacyProducesSEIPDv1 = do- (recipient, privateKey) <- loadUnencryptedRsaSigner- let iv = IV (B.replicate 16 0xAB)- payload = "legacy interop payload"- request =- RecipientEncryptRequest- { recipientEncryptRequestTargets = [recipientEncryptionTarget recipient]- , recipientEncryptRequestPayloadShape = defaultRecipientPayloadShape- , recipientEncryptRequestPayload = payload- , recipientEncryptRequestSymmetricOverride = Just AES256- , recipientEncryptRequestOverrides =- RecipientEncryptRequestSEIPDv1Overrides- { recipientEncryptRequestIVOverride = Just iv- }- }- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipient- , pkeskRecipientSKey = RSAPrivateKey (RSA_PrivateKey privateKey)- }))- passphraseCallback _ = pure BL.empty- result <- encryptForRecipients request- packets <-- case result of- Left err ->- assertFailure- ("Expected EncryptInteropLegacy request to succeed, got " ++ show err) >>- fail "encryptForRecipients failed"- Right RecipientEncryptResult { recipientEncryptPackets = ps } -> pure ps- let seipdPkts = [p | SymEncIntegrityProtectedDataPkt p <- packets]- case seipdPkts of- [SEIPD1 1 _] -> pure ()- [SEIPD2 {}] -> assertFailure "Expected SEIPDv1 but got SEIPDv2"- other -> assertFailure ("Unexpected SEIPD packets: " ++ show other)- let pkeskPkts = [p | PKESKPkt p <- packets]- assertBool "EncryptInteropLegacy should emit PKESKv3 packets" $- all (\p -> case p of { PKESKPayloadV3Packet {} -> True; _ -> False }) pkeskPkts- decrypted <-- DC.runConduitRes $- CL.sourceList packets DC..|- conduitDecryptWithPKESKContext keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual- "decrypted legacy interop payload matches original"- (BL.fromStrict payload)- gotPayload- other ->- assertFailure ("Expected [LiteralData] from legacy decrypt, got " ++ show other)--testCanonicalizePKESKRecipientIdHelper :: Assertion-testCanonicalizePKESKRecipientIdHelper = do- let rid = BL.pack (0x04 : replicate 20 0x11)- payload = PKESKPayloadV6Packet (PKESKPayloadV6 rid RSA "esk")- case canonicalizePKESKRecipientId payload of- Right (PKESKPayloadV6Packet (PKESKPayloadV6 normalized _ _))- | BL.length normalized == 20 -> pure ()- | otherwise ->- assertFailure ("Expected canonicalized recipient id length 20, got " ++ show (BL.length normalized))- Left err ->- assertFailure ("canonicalizePKESKRecipientId failed unexpectedly: " ++ show err)- Right other ->- assertFailure ("Expected PKESK6 payload after canonicalization, got " ++ show other)- case canonicalizePKESKRecipientId (PKESKPayloadV6Packet (PKESKPayloadV6 (BL.replicate 19 0x22) RSA "esk")) of- Left (InvalidRecipientIdentifier _) -> pure ()- other ->- assertFailure ("Expected InvalidRecipientIdentifier for malformed rid, got " ++ show other)- case canonicalizePKESKRecipientId (PKESKPayloadV6Packet (PKESKPayloadV6 (BL.pack (0x06 : replicate 32 0x33)) RSA "esk")) of- Right (PKESKPayloadV6Packet (PKESKPayloadV6 normalized _ _))- | BL.length normalized == 32 -> pure ()- | otherwise ->- assertFailure ("Expected canonicalized v6 recipient id length 32, got " ++ show (BL.length normalized))- other ->- assertFailure ("Expected 0x06-prefixed v6 recipient id to canonicalize, got " ++ show other)- case canonicalizePKESKRecipientId (PKESKPayloadV6Packet (PKESKPayloadV6 (BL.pack (0x04 : replicate 32 0x44)) RSA "esk")) of- Left (InvalidRecipientIdentifier _) -> pure ()- other ->- assertFailure- ("Expected InvalidRecipientIdentifier for mismatched v4-prefixed v6-length rid, got " ++- show other)--testBuildPKESKPayloadUnsupportedRecipient :: Assertion-testBuildPKESKPayloadUnsupportedRecipient = do- let recipient =- PKPayload- V6- (ThirtyTwoBitTimeStamp 0)- 0- EdDSA- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint 1)))- sessionKey = SessionKey (B.replicate 32 0x19)- sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey- result <- buildPKESKPayloadForRecipient PreferV6 recipient sessionMaterial- case result of- Left (UnsupportedRecipientAlgorithm EdDSA) -> pure ()- Left err ->- assertFailure ("Expected UnsupportedRecipientAlgorithm EdDSA, got " ++ show err)- Right payload ->- assertFailure ("Expected UnsupportedRecipientAlgorithm, got payload: " ++ show payload)--testBuildPKESKPayloadRejectsECDHSHA1 :: Assertion-testBuildPKESKPayloadRejectsECDHSHA1 = do- let (recipientPub, _) = syntheticNISTP256Low- recipient =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- ECDH- (ECDHPubKey (ECDSAPubKey (ECDSA_PublicKey recipientPub)) SHA1 AES256)- sessionKey = SessionKey (B.replicate 32 0x19)- sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey- 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 err ->- assertFailure ("Expected RecipientKdfFailure ECDH for SHA1, got " ++ show err)- Right payload ->- assertFailure ("Expected SHA1 policy rejection, got payload: " ++ show payload)--testConduitDecryptSEIPDv2FixtureWithMatchingV6SecretKey :: Assertion-testConduitDecryptSEIPDv2FixtureWithMatchingV6SecretKey = do- armoredMessage <- readFixtureStrict "seipdv2.pgp.aa"- armoredEncryptedSecret <- readFixtureStrict "v6-encrypted-secret.pgp.aa"- armoredPlainSecret <- readFixtureStrict "v6-secret.pgp.aa"- passphrase <- readPKIPassphrase- messageArmors <-- case (AA.decode armoredMessage :: Either String [Armor]) of- Left err -> assertFailure ("failed to decode seipdv2 armor: " ++ err) >> pure []- Right as -> pure as- encryptedSecretArmors <-- case (AA.decode armoredEncryptedSecret :: Either String [Armor]) of- Left err ->- assertFailure ("failed to decode v6 encrypted secret armor: " ++ err) >> pure []- Right as -> pure as- plainSecretArmors <-- case (AA.decode armoredPlainSecret :: Either String [Armor]) of- Left err ->- assertFailure ("failed to decode v6 plain secret armor: " ++ err) >> pure []- Right as -> pure as- messageBody <-- case messageArmors of- (a:_) -> pure (armorPayload a)- [] -> assertFailure "seipdv2 armor file contained no armor blocks" >> pure mempty- encryptedSecretBody <-- case encryptedSecretArmors of- (a:_) -> pure (armorPayload a)- [] ->- assertFailure "v6-encrypted-secret armor file contained no armor blocks" >>- pure mempty- plainSecretBody <-- case plainSecretArmors of- (a:_) -> pure (armorPayload a)- [] ->- assertFailure "v6-secret armor file contained no armor blocks" >>- pure mempty- let messagePackets = parsePkts messageBody- encryptedSecretPackets = parsePkts encryptedSecretBody- plainSecretPackets = parsePkts plainSecretBody- passphraseCallback _ = pure BL.empty- encryptedKeyInfos <- collectSecretKeyInfos encryptedSecretPackets passphrase- plainKeyInfos <- collectSecretKeyInfos plainSecretPackets passphrase- let allKeyInfos = encryptedKeyInfos ++ plainKeyInfos- keyContextCallback pkt = pure (selectRecipientKeyInfo pkt allKeyInfos)- recipientKeyInfo =- case [pkt | pkt@(PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 _ _ _))) <- messagePackets] of- (pkt:_) -> selectRecipientKeyInfo pkt allKeyInfos- [] -> listToMaybe allKeyInfos- case recipientKeyInfo of- Nothing ->- assertFailure- ("no usable secret key material found in v6-encrypted-secret fixture: " ++- show- ([(_pkalgo pkp, ska) | SecretKeyPkt pkp ska <- encryptedSecretPackets] ++- [(_pkalgo pkp, ska) | SecretSubkeyPkt pkp ska <- encryptedSecretPackets]))- Just _ -> do- decrypted <-- DC.runConduitRes $- CL.sourceList messagePackets DC..|- conduitDecryptWithPKESKContext keyContextCallback passphraseCallback DC..|- CL.consume- case [p | LiteralDataPkt _ _ _ p <- decrypted] of- (payload:_) ->- if BL.null payload- then assertFailure "fixture decrypt produced empty literal payload"- else pure ()- [] ->- assertFailure- ("expected decrypted literal payload from seipdv2 fixture, got: " ++- show decrypted)--testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKey :: Assertion-testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKey = do- (messagePackets, encryptedSecretPackets, passphrase) <-- loadSEIPDv2FixtureWithV4Secret "seipdv2-for-v4-key.pgp.aa"- let- passphraseCallback _ = pure BL.empty- keyInfos <- collectSecretKeyInfos encryptedSecretPackets passphrase- let keyContextCallback pkt = pure (selectRecipientKeyInfo pkt keyInfos)- decrypted <-- DC.runConduitRes $- CL.sourceList messagePackets DC..|- conduitDecryptWithPKESKContext keyContextCallback passphraseCallback DC..|- CL.consume- case [p | LiteralDataPkt _ _ _ p <- decrypted] of- (payload:_) ->- if "This is a test of SEIPDv1" `isInfixOf` BLC8.unpack payload- then pure ()- else- assertFailure- ("unexpected decrypted payload for seipdv2-for-v4-key fixture: " ++- show payload)- [] ->- assertFailure- ("expected decrypted literal payload from seipdv2-for-v4-key fixture, got: " ++- show decrypted)--testConduitDecryptSEIPDv2FixtureIgnoresUnusableLatestPKESK :: Assertion-testConduitDecryptSEIPDv2FixtureIgnoresUnusableLatestPKESK = do- (messagePackets, encryptedSecretPackets, passphrase) <-- loadSEIPDv2FixtureWithV4Secret "seipdv2-for-v4-key.pgp.aa"- let- bogusRid = BL.pack (0x06 : replicate 32 0x99)- bogusPKESK = PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 bogusRid RSA "bogus-esk"))- (eskPrefix, encryptedSuffix) = span isPrecedingESK messagePackets- packetsWithBogusLatestPKESK = eskPrefix ++ [bogusPKESK] ++ encryptedSuffix- passphraseCallback _ = pure BL.empty- keyInfos <- collectSecretKeyInfos encryptedSecretPackets passphrase- let keyContextCallback pkt = pure (selectRecipientKeyInfo pkt keyInfos)- decrypted <-- DC.runConduitRes $- CL.sourceList packetsWithBogusLatestPKESK DC..|- conduitDecryptWithPKESKContext keyContextCallback passphraseCallback DC..|- CL.consume- case [p | LiteralDataPkt _ _ _ p <- decrypted] of- (payload:_) ->- if "This is a test of SEIPDv1" `isInfixOf` BLC8.unpack payload- then pure ()- else- assertFailure- ("unexpected decrypted payload for seipdv2-for-v4-key fixture with bogus latest PKESK: " ++- show payload)- [] ->- assertFailure- ("expected decrypted literal payload with bogus latest PKESK, got: " ++- show decrypted)---testConduitDecryptSEIPDv2FixtureAcceptsRecipientIdWithoutCallerPermutations :: Assertion-testConduitDecryptSEIPDv2FixtureAcceptsRecipientIdWithoutCallerPermutations = do- (messagePacketsRaw, encryptedSecretPackets, passphrase) <-- loadSEIPDv2FixtureWithV4Secret "seipdv2-for-v4-key.pgp.aa"- let messagePackets = map forceVersionedRecipientIdentifier messagePacketsRaw- passphraseCallback _ = pure BL.empty- keyInfos <- collectSecretKeyInfos encryptedSecretPackets passphrase- let keyContextCallback pkt = pure (selectRecipientKeyInfoByRawRecipientId pkt keyInfos)- decrypted <-- DC.runConduitRes $- CL.sourceList messagePackets DC..|- conduitDecryptWithPKESKContext keyContextCallback passphraseCallback DC..|- CL.consume- case [p | LiteralDataPkt _ _ _ p <- decrypted] of- (payload:_) ->- if "This is a test of SEIPDv1" `isInfixOf` BLC8.unpack payload- then pure ()- else- assertFailure- ("unexpected decrypted payload for recipient-id normalization fixture: " ++- show payload)- [] ->- assertFailure- ("expected decrypted literal payload for recipient-id normalization fixture, got: " ++- show decrypted)--testConduitDecryptPKESKFailureDoesNotRequireManualSessionMaterial :: Assertion-testConduitDecryptPKESKFailureDoesNotRequireManualSessionMaterial = do- armors <- loadArmor "seipdv2-for-v4-key.pgp.aa"- armor <-- case armors of- (a:_) -> pure a- [] ->- assertFailure "seipdv2-for-v4-key.pgp.aa should contain one armored payload" >>- fail "expected one armored payload"- let packets = parsePkts (armorPayload armor)- keyContextCallback _ = pure Nothing- passphraseCallback prompt =- fail ("unexpected manual PKESK session key prompt: " ++ prompt)- result <-- catch- (Right <$> (DC.runConduitRes $ CL.sourceList packets DC..| conduitDecryptWithPKESKContext keyContextCallback passphraseCallback DC..| CL.consume))- (\e -> pure (Left (show (e :: SomeException))))- case result of- Left err -> do- assertBool- "failure should report PKESK candidate exhaustion"- ("no matching key context" `isInfixOf` err)- assertBool- "failure should not request manual PKESK session material"- (not ("unexpected manual PKESK session key prompt" `isInfixOf` err))- Right decrypted ->- assertFailure- ("expected decryption failure when no PKESK key context is available, got: " ++- show decrypted)--testConduitDecryptSEIPDv2TwoRecipientsFixtureWithMatchingV4SecretKey :: Assertion-testConduitDecryptSEIPDv2TwoRecipientsFixtureWithMatchingV4SecretKey =- testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform- "seipdv2-two-recipients.pgp.aa"- id--testConduitDecryptSEIPDv2ThreeRecipientsFixtureWithMatchingV4SecretKey :: Assertion-testConduitDecryptSEIPDv2ThreeRecipientsFixtureWithMatchingV4SecretKey =- testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform- "seipdv2-three-recipients.pgp.aa"- id--testConduitDecryptSEIPDv2TwoRecipientsFixtureWithReorderedAndUnusablePKESKs :: Assertion-testConduitDecryptSEIPDv2TwoRecipientsFixtureWithReorderedAndUnusablePKESKs =- testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform- "seipdv2-two-recipients.pgp.aa"- (prependUnusableLatestPKESK . reorderPrecedingPKESKs)--testConduitDecryptSEIPDv2ThreeRecipientsFixtureWithReorderedAndUnusablePKESKs :: Assertion-testConduitDecryptSEIPDv2ThreeRecipientsFixtureWithReorderedAndUnusablePKESKs =- testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform- "seipdv2-three-recipients.pgp.aa"- (prependUnusableLatestPKESK . reorderPrecedingPKESKs)--testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFile :: FilePath -> Assertion-testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFile fixture =- testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform- fixture- id--testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform ::- FilePath- -> ([Pkt] -> [Pkt])- -> Assertion-testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform fixture transformPackets = do- (messagePacketsRaw, encryptedSecretPackets, passphrase) <- loadSEIPDv2FixtureWithV4Secret fixture- let messagePackets = transformPackets messagePacketsRaw- passphraseCallback _ = pure BL.empty- keyInfos <- collectSecretKeyInfos encryptedSecretPackets passphrase- let keyContextCallback pkt = pure (selectRecipientKeyInfo pkt keyInfos)- decrypted <-- DC.runConduitRes $- CL.sourceList messagePackets DC..|- conduitDecryptWithPKESKContext keyContextCallback passphraseCallback DC..|- CL.consume- case [p | LiteralDataPkt _ _ _ p <- decrypted] of- (payload:_) ->- if BL.null payload- then assertFailure (fixture ++ " decrypt produced empty literal payload")- else pure ()- [] ->- assertFailure- ("expected decrypted literal payload from " ++ fixture ++ ", got: " ++- show decrypted)--testConduitDecryptSEIPDv2WithPKESKv6RawSessionKey :: Assertion-testConduitDecryptSEIPDv2WithPKESKv6RawSessionKey = do- let sessionKey = B.replicate 32 0x2a- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "v6 pkesk session key decrypt path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- packets ct =- [ PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 "\x01\x02\x03\x04\x05\x06\x07\x08" RSA "\x99\x88\x77"))- , SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ct))- ]- callback _ = pure (BL.fromStrict sessionKey)- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt (SessionKey sessionKey) (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList (packets ciphertext) DC..| conduitDecrypt callback DC..| CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "PKESKv6 raw session-key decrypt payload" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)--testConduitDecryptSEIPDv2RejectsWrongPKESKv6RawSessionKeyLength :: Assertion-testConduitDecryptSEIPDv2RejectsWrongPKESKv6RawSessionKeyLength = do- let sessionKey = B.replicate 32 0x2a- badSessionKey = B.replicate 31 0x2a- salt = Salt (B.pack [0x00 .. 0x1f])- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) "payload"]- packets ct =- [ PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 "\x10\x11\x12\x13\x14\x15\x16\x17" RSA "\x01\x02\x03"))- , SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ct))- ]- callback _ = pure (BL.fromStrict badSessionKey)- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt (SessionKey sessionKey) (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- result <-- catch- (Right <$>- (DC.runConduitRes $- CL.sourceList (packets ciphertext) DC..| conduitDecrypt callback DC..| CL.consume))- (\e -> pure (Left (show (e :: SomeException))))- case result of- Left err ->- if "PKESK raw session key length does not match payload algorithm" `isInfixOf` err- then return ()- else- assertFailure- ("Expected PKESKv6 raw session-key length validation failure, got: " ++ err)- Right got ->- assertFailure- ("Expected PKESKv6 raw session-key length failure, got packets: " ++ show got)--testConduitDecryptSEIPDv2WithPKESKRSAUnwrap :: Assertion-testConduitDecryptSEIPDv2WithPKESKRSAUnwrap = do- (baseRecipient, privateKey) <- loadUnencryptedRsaSigner- publicKey <-- case _pubkey baseRecipient of- RSAPubKey (RSA_PublicKey pub) -> pure pub- other ->- assertFailure- ("unencrypted.seckey did not contain an RSA recipient, got " ++ show other) >>- fail "expected RSA recipient"- let sessionKey = B.replicate 32 0x2b- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk rsa unwrap decrypt path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback _ = do- let msk = Just (RSAPrivateKey (RSA_PrivateKey privateKey))- pure $- fmap- (\sk ->- PKESKRecipientKey- { pkeskRecipientPKPayload = Nothing- , pkeskRecipientSKey = sk- })- msk- encryptedResult <- (P15.encrypt publicKey sessionKey :: IO (Either RSA.Error B.ByteString))- encryptedSessionMaterial <-- case encryptedResult of- Left err -> assertFailure ("RSA PKESK encryption failed: " ++ show err) >> pure mempty- Right ct -> pure ct- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt (SessionKey sessionKey) (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- let pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId "\x11\x22\x33\x44\x55\x66\x77\x88")- RSA- (MPI (os2ip encryptedSessionMaterial) :| [])))- decrypted <-- DC.runConduitRes $- CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "PKESK RSA unwrap decrypt payload" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)--testConduitDecryptSEIPDv2WithPKESKRSAUnwrapViaWildcardCallbackFallback :: Assertion-testConduitDecryptSEIPDv2WithPKESKRSAUnwrapViaWildcardCallbackFallback = do- (baseRecipient, privateKey) <- loadUnencryptedRsaSigner- publicKey <-- case _pubkey baseRecipient of- RSAPubKey (RSA_PublicKey pub) -> pure pub- other ->- assertFailure- ("unencrypted.seckey did not contain an RSA recipient, got " ++ show other) >>- fail "expected RSA recipient"- let sessionKey = B.replicate 32 0x2e- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk rsa unwrap decrypt path via wildcard callback fallback"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback pkt =- case pkt of- PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 _ (EightOctetKeyId rid) _ _))- | BL.length rid == 8 && BL.all (== 0) rid ->- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Nothing- , pkeskRecipientSKey =- RSAPrivateKey (RSA_PrivateKey privateKey)- }))- _ -> pure Nothing- encryptedResult <- (P15.encrypt publicKey sessionKey :: IO (Either RSA.Error B.ByteString))- encryptedSessionMaterial <-- case encryptedResult of- Left err -> assertFailure ("RSA PKESK encryption failed: " ++ show err) >> pure mempty- Right ct -> pure ct- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt (SessionKey sessionKey) (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- let pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId "\xaa\xbb\xcc\xdd\xee\xff\x00\x11")- RSA- (MPI (os2ip encryptedSessionMaterial) :| [])))- decrypted <-- DC.runConduitRes $- CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "PKESK RSA unwrap via wildcard callback fallback payload" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)--testConduitDecryptSEIPDv2WithPKESKRSAUnwrapFromProtectedKey :: Assertion-testConduitDecryptSEIPDv2WithPKESKRSAUnwrapFromProtectedKey = do- passphrase <- readPKIPassphrase- secretPackets <-- DC.runConduitRes $- CB.sourceFile "tests/data/aes256-sha512.seckey" DC..| conduitGet get DC..|- CL.consume- keyInfos <- collectSecretKeyInfos secretPackets passphrase- keyInfo <-- case keyInfos of- (ki:_) -> pure ki- [] ->- assertFailure- "aes256-sha512.seckey should yield at least one RSA key context" >>- fail "expected key info"- publicKey <-- case pkeskRecipientPKPayload keyInfo of- Nothing ->- assertFailure "key info from file should include a public key payload" >>- fail "expected public key payload"- Just pkp ->- case _pubkey pkp of- RSAPubKey (RSA_PublicKey pub) -> pure pub- other ->- assertFailure- ("expected RSA public key from aes256-sha512.seckey, got " ++ show other) >>- fail "expected RSA public key"- let sessionKey = B.replicate 32 0x42- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk rsa unwrap via sha1-cfb protected key loaded from file"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback pkt = pure (selectRecipientKeyInfo pkt keyInfos)- encryptedResult <- (P15.encrypt publicKey sessionKey :: IO (Either RSA.Error B.ByteString))- encryptedSessionMaterial <-- case encryptedResult of- Left err ->- assertFailure ("RSA PKESK encryption failed: " ++ show err) >> pure mempty- Right ct -> pure ct- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt (SessionKey sessionKey) (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- let pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId "\x11\x22\x33\x44\x55\x66\x77\x88")- RSA- (MPI (os2ip encryptedSessionMaterial) :| [])))- decrypted <-- DC.runConduitRes $- CL.sourceList- [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithPKESKContext keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual- "PKESK RSA unwrap with SHA1-CFB protected key from file"- payload- gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)--testConduitDecryptSEIPDv2WithPKESKECDHUnwrap :: Assertion-testConduitDecryptSEIPDv2WithPKESKECDHUnwrap = do- let (recipientPub, recipientPriv) = syntheticNISTP256Low- (ephPub, ephPriv) = syntheticNISTP256High- recipientPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- ECDH- (ECDHPubKey (ECDSAPubKey (ECDSA_PublicKey recipientPub)) SHA256 AES128)- recipientSKey = ECDHPrivateKey (ECDSA_PrivateKey recipientPriv)- sessionKey = B.replicate 32 0x2c- encodedSession =- B.singleton (fromFVal AES256) <>- sessionKey <>- encodeChecksum16 sessionKey <>- B.replicate 5 0- sharedSecret =- BA.convert- (ECCDH.getShared- (ECDSA.public_curve recipientPub)- (ECDSA.private_d ephPriv)- (ECDSA.public_q recipientPub)) :: B.ByteString- kdfParam =- buildECDHKDFParamForTest recipientPKP ECDH (ECDSA.public_curve recipientPub) SHA256 AES128- kek = deriveECDHKekForTest SHA256 AES128 sharedSecret kdfParam- wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek encodedSession- ephPointBytes =- maybe- (error "failed to encode ephemeral point")- id- (point2MBS (ECDSA.public_q ephPub))- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId "\x99\x88\x77\x66\x55\x44\x33\x22")- ECDH- (MPI (os2ip ephPointBytes) :| [MPI (os2ip wrappedSession)])))- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk ecdh unwrap decrypt path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipientPKP- , pkeskRecipientSKey = recipientSKey- }))- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt (SessionKey sessionKey) (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "PKESK ECDH unwrap decrypt payload" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)--testConduitDecryptSEIPDv2RejectsECDHWrongEphemeralPointLength :: Assertion-testConduitDecryptSEIPDv2RejectsECDHWrongEphemeralPointLength = do- let (recipientPub, recipientPriv) = syntheticNISTP256Low- (ephPub, ephPriv) = syntheticNISTP256High- recipientPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- ECDH- (ECDHPubKey (ECDSAPubKey (ECDSA_PublicKey recipientPub)) SHA256 AES128)- recipientSKey = ECDHPrivateKey (ECDSA_PrivateKey recipientPriv)- sessionKey = B.replicate 32 0x2c- encodedSession =- B.singleton (fromFVal AES256) <>- sessionKey <>- encodeChecksum16 sessionKey <>- B.replicate 5 0- sharedSecret =- BA.convert- (ECCDH.getShared- (ECDSA.public_curve recipientPub)- (ECDSA.private_d ephPriv)- (ECDSA.public_q recipientPub)) :: B.ByteString- kdfParam =- buildECDHKDFParamForTest recipientPKP ECDH (ECDSA.public_curve recipientPub) SHA256 AES128- kek = deriveECDHKekForTest SHA256 AES128 sharedSecret kdfParam- wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek encodedSession- ephPointBytes =- maybe- (error "failed to encode ephemeral point")- id- (point2MBS (ECDSA.public_q ephPub))- truncatedEphemeral = B.take (B.length ephPointBytes - 1) ephPointBytes- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId "\x99\x88\x77\x66\x55\x44\x33\x22")- ECDH- (MPI (os2ip truncatedEphemeral) :| [MPI (os2ip wrappedSession)])))- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk ecdh wrong ephemeral point length"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipientPKP- , pkeskRecipientSKey = recipientSKey- }))- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt (SessionKey sessionKey) (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- result <-- catch- (Right <$>- DC.runConduitRes- (CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume))- (\e -> pure (Left (show (e :: SomeException))))- case result of- Left err ->- assertBool- "ECDH unwrap should reject wrong uncompressed point length for recipient curve"- ("invalid length for recipient curve" `isInfixOf` err)- Right packets ->- assertFailure ("Expected ECDH point-length validation failure, got: " ++ show packets)--testConduitDecryptSEIPDv2WithPKESKX25519V3Unwrap :: Assertion-testConduitDecryptSEIPDv2WithPKESKX25519V3Unwrap = do- let recipientSecretRaw = B.pack [0x21 .. 0x40]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize X25519 recipient secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- recipientPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip recipientPublicRaw))))- recipientSKey = X25519PrivateKey recipientSecretRaw- ephSecretRaw = B.pack [0x41 .. 0x60]- ephSecret =- case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize X25519 ephemeral secret key: " ++ show err)- Right sk -> sk- ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- recipientPub =- case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of- Left err -> error ("failed to initialize X25519 recipient public key: " ++ show err)- Right pk -> pk- sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString- kek = deriveX25519KekForTest ephPublicRaw recipientPublicRaw sharedSecret- sessionKey = SessionKey (B.replicate 32 0x6a)- wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)- eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId "\x12\x34\x56\x78\x90\xab\xcd\xef")- X25519- (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])))- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk x25519 v3 unwrap decrypt path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipientPKP- , pkeskRecipientSKey = recipientSKey- }))- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "PKESK X25519 v3 unwrap decrypt payload" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)---testConduitDecryptSEIPDv2RetriesWildcardPKESKv3AcrossRecipientKeyOrder :: Assertion-testConduitDecryptSEIPDv2RetriesWildcardPKESKv3AcrossRecipientKeyOrder = do- let recipientSecretRaw = B.pack [0x21 .. 0x40]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize X25519 recipient secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- recipientPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip recipientPublicRaw))))- recipientSKey = X25519PrivateKey recipientSecretRaw- wrongRecipientSecretRaw = B.pack [0x61 .. 0x80]- wrongRecipientSecret =- case CE.eitherCryptoError (C25519.secretKey wrongRecipientSecretRaw) of- Left err -> error ("failed to initialize wrong X25519 recipient secret key: " ++ show err)- Right sk -> sk- wrongRecipientPublicRaw = BA.convert (C25519.toPublic wrongRecipientSecret) :: B.ByteString- wrongRecipientPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip wrongRecipientPublicRaw))))- wrongRecipientSKey = X25519PrivateKey wrongRecipientSecretRaw- ephSecretRaw = B.pack [0x41 .. 0x60]- ephSecret =- case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize X25519 ephemeral secret key: " ++ show err)- Right sk -> sk- ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- recipientPub =- case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of- Left err -> error ("failed to initialize X25519 recipient public key: " ++ show err)- Right pk -> pk- sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString- kek = deriveX25519KekForTest ephPublicRaw recipientPublicRaw sharedSecret- sessionKey = SessionKey (B.replicate 32 0x6b)- wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)- eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId (BL.replicate 8 0))- X25519- (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])))- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk x25519 v3 wildcard key-id retry path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- wrongKeyInfo =- PKESKRecipientKey- { pkeskRecipientPKPayload = Just wrongRecipientPKP- , pkeskRecipientSKey = wrongRecipientSKey- }- correctKeyInfo =- PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipientPKP- , pkeskRecipientSKey = recipientSKey- }- keyContextCallback _rid _pka = pure [wrongKeyInfo, correctKeyInfo]- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithCandidatesCallbackAndPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "PKESKv3 wildcard-keyid retries across recipient key order" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)---testConduitDecryptSEIPDv2RetriesWildcardPKESKv3AcrossLongRecipientKeyOrder :: Assertion-testConduitDecryptSEIPDv2RetriesWildcardPKESKv3AcrossLongRecipientKeyOrder = do- let recipientSecretRaw = B.pack [0x21 .. 0x40]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize X25519 recipient secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- ephSecretRaw = B.pack [0x41 .. 0x60]- ephSecret =- case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize X25519 ephemeral secret key: " ++ show err)- Right sk -> sk- ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- recipientPub =- case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of- Left err -> error ("failed to initialize X25519 recipient public key: " ++ show err)- Right pk -> pk- sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString- kek = deriveX25519KekForTest ephPublicRaw recipientPublicRaw sharedSecret- sessionKey = SessionKey (B.replicate 32 0x6c)- wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)- eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId (BL.replicate 8 0))- X25519- (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])))- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk x25519 v3 wildcard key-id long retry path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- mkSecretBytes offset =- B.pack [fromIntegral ((offset + i) `mod` 256) | i <- [0 .. 31 :: Int]]- mkX25519RecipientKeyInfo label secretRaw =- let sk =- case CE.eitherCryptoError (C25519.secretKey secretRaw) of- Left err -> error ("failed to initialize " ++ label ++ " X25519 secret key: " ++ show err)- Right secretKey -> secretKey- publicRaw = BA.convert (C25519.toPublic sk) :: B.ByteString- in PKESKRecipientKey- { pkeskRecipientPKPayload =- Just- (PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip publicRaw)))))- , pkeskRecipientSKey = X25519PrivateKey secretRaw- }- wrongKeyInfos =- [ mkX25519RecipientKeyInfo ("wrong-" ++ show n) (mkSecretBytes (96 + n * 7))- | n <- [1 .. 20 :: Int]- ]- correctKeyInfo = mkX25519RecipientKeyInfo "correct" recipientSecretRaw- keyContextCallback _rid _pka = pure (wrongKeyInfos ++ [correctKeyInfo])- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithCandidatesCallbackAndPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "PKESKv3 wildcard-keyid retries long recipient key order" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)---testConduitDecryptSEIPDv1RetriesWildcardPKESKv3AcrossLongRecipientKeyOrder :: Assertion-testConduitDecryptSEIPDv1RetriesWildcardPKESKv3AcrossLongRecipientKeyOrder = do- let recipientSecretRaw = B.pack [0x21 .. 0x40]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize X25519 recipient secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- ephSecretRaw = B.pack [0x41 .. 0x60]- ephSecret =- case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize X25519 ephemeral secret key: " ++ show err)- Right sk -> sk- ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- recipientPub =- case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of- Left err -> error ("failed to initialize X25519 recipient public key: " ++ show err)- Right pk -> pk- sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString- kek = deriveX25519KekForTest ephPublicRaw recipientPublicRaw sharedSecret- sessionKey = SessionKey (B.replicate 32 0x6d)- wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)- eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId (BL.replicate 8 0))- X25519- (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])))- payload = "pkesk x25519 v3 wildcard key-id long retry legacy seipd1 path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- cleartext = BL.toStrict (runPut (put literalBlock))- iv = IV (B.pack [0x33 .. 0x42])- cleartextWithMDC = cleartext <> mdcTrailerForSEIPDv1 iv cleartext- passphraseCallback _ = pure BL.empty- mkSecretBytes offset =- B.pack [fromIntegral ((offset + i) `mod` 256) | i <- [0 .. 31 :: Int]]- mkX25519RecipientKeyInfo label secretRaw =- let sk =- case CE.eitherCryptoError (C25519.secretKey secretRaw) of- Left err -> error ("failed to initialize " ++ label ++ " X25519 secret key: " ++ show err)- Right secretKey -> secretKey- publicRaw = BA.convert (C25519.toPublic sk) :: B.ByteString- in PKESKRecipientKey- { pkeskRecipientPKPayload =- Just- (PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip publicRaw)))))- , pkeskRecipientSKey = X25519PrivateKey secretRaw- }- wrongKeyInfos =- [ mkX25519RecipientKeyInfo ("wrong-" ++ show n) (mkSecretBytes (96 + n * 7))- | n <- [1 .. 20 :: Int]- ]- correctKeyInfo = mkX25519RecipientKeyInfo "correct" recipientSecretRaw- keyContextCallback _rid _pka = pure (wrongKeyInfos ++ [correctKeyInfo])- ciphertext <-- either- (\e -> assertFailure ("encryptOpenPGPCfbRaw failed: " ++ show e) >> pure mempty)- pure- (encryptOpenPGPCfbRaw OpenPGPCFBNoResyncW AES256 iv cleartextWithMDC (unSessionKey sessionKey))- decrypted <-- DC.runConduitRes $- CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD1 1 (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithCandidatesCallbackAndPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "PKESKv3 wildcard-keyid retries long recipient key order for SEIPDv1" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)---testMkCandidateResolverDecryptsWildcardPKESKv3 :: Assertion-testMkCandidateResolverDecryptsWildcardPKESKv3 = do- let recipientSecretRaw = B.pack [0x21 .. 0x40]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize X25519 recipient secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- recipientPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip recipientPublicRaw))))- recipientSKey = X25519PrivateKey recipientSecretRaw- wrongSecretRaw = B.pack [0x61 .. 0x80]- wrongSecret =- case CE.eitherCryptoError (C25519.secretKey wrongSecretRaw) of- Left err -> error ("failed to initialize wrong X25519 secret key: " ++ show err)- Right sk -> sk- wrongPublicRaw = BA.convert (C25519.toPublic wrongSecret) :: B.ByteString- wrongPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip wrongPublicRaw))))- wrongSKey = X25519PrivateKey wrongSecretRaw- ephSecretRaw = B.pack [0x41 .. 0x60]- ephSecret =- case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize X25519 ephemeral secret key: " ++ show err)- Right sk -> sk- ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- recipientPub =- case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of- Left err -> error ("failed to initialize X25519 recipient public key: " ++ show err)- Right pk -> pk- sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString- kek = deriveX25519KekForTest ephPublicRaw recipientPublicRaw sharedSecret- sessionKey = SessionKey (B.replicate 32 0x6d)- wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)- eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId (BL.replicate 8 0))- X25519- (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])))- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "unwrap callback wildcard path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- wrongKeyInfo =- PKESKRecipientKey {pkeskRecipientPKPayload = Just wrongPKP, pkeskRecipientSKey = wrongSKey}- correctKeyInfo =- PKESKRecipientKey {pkeskRecipientPKPayload = Just recipientPKP, pkeskRecipientSKey = recipientSKey}- let resolver _rid _pka = pure [wrongKeyInfo, correctKeyInfo]- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- (void $- DCD.conduitDecrypt- DCD.DecryptOptions- { DCD.decryptOptionsKeyResolution = DCD.DecryptWithUnwrapCandidatesCallback resolver- , DCD.decryptOptionsPolicy = lenientDecryptPolicy- , DCD.decryptOptionsPassphraseCallback = passphraseCallback- }) DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "unwrap callback should decrypt wildcard PKESK via candidate list" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)---testConduitDecryptWildcardResolverProvidesTypedPreviousFailures :: Assertion-testConduitDecryptWildcardResolverProvidesTypedPreviousFailures = do- let recipientSecretRaw = B.pack [0x21 .. 0x40]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize X25519 recipient secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- recipientPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip recipientPublicRaw))))- recipientSKey = X25519PrivateKey recipientSecretRaw- wrongSecretRaw = B.pack [0x61 .. 0x80]- wrongSecret =- case CE.eitherCryptoError (C25519.secretKey wrongSecretRaw) of- Left err -> error ("failed to initialize wrong X25519 secret key: " ++ show err)- Right sk -> sk- wrongPublicRaw = BA.convert (C25519.toPublic wrongSecret) :: B.ByteString- wrongPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip wrongPublicRaw))))- wrongSKey = X25519PrivateKey wrongSecretRaw- ephSecretRaw = B.pack [0x41 .. 0x60]- ephSecret =- case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize X25519 ephemeral secret key: " ++ show err)- Right sk -> sk- ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- recipientPub =- case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of- Left err -> error ("failed to initialize X25519 recipient public key: " ++ show err)- Right pk -> pk- sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString- kek = deriveX25519KekForTest ephPublicRaw recipientPublicRaw sharedSecret- sessionKey = SessionKey (B.replicate 32 0x6e)- wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)- eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId (BL.replicate 8 0))- X25519- (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])))- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "typed previous failures for wildcard retries"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- wrongKeyInfo =- PKESKRecipientKey {pkeskRecipientPKPayload = Just wrongPKP, pkeskRecipientSKey = wrongSKey}- correctKeyInfo =- PKESKRecipientKey {pkeskRecipientPKPayload = Just recipientPKP, pkeskRecipientSKey = recipientSKey}- let resolver _rid _pka = pure [wrongKeyInfo, correctKeyInfo]- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- (void $- DCD.conduitDecrypt- DCD.DecryptOptions- { DCD.decryptOptionsKeyResolution = DCD.DecryptWithUnwrapCandidatesCallback resolver- , DCD.decryptOptionsPolicy = lenientDecryptPolicy- , DCD.decryptOptionsPassphraseCallback = passphraseCallback- }) DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "wildcard retries with typed previous failures should still decrypt" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)- pure ()---testConduitDecryptWithReportCapturesWildcardResolverDiagnostics :: Assertion-testConduitDecryptWithReportCapturesWildcardResolverDiagnostics = do- let recipientSecretRaw = B.pack [0x21 .. 0x40]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize X25519 recipient secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- recipientPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip recipientPublicRaw))))- recipientSKey = X25519PrivateKey recipientSecretRaw- wrongSecretRaw = B.pack [0x61 .. 0x80]- wrongSecret =- case CE.eitherCryptoError (C25519.secretKey wrongSecretRaw) of- Left err -> error ("failed to initialize wrong X25519 secret key: " ++ show err)- Right sk -> sk- wrongPublicRaw = BA.convert (C25519.toPublic wrongSecret) :: B.ByteString- wrongPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip wrongPublicRaw))))- wrongSKey = X25519PrivateKey wrongSecretRaw- ephSecretRaw = B.pack [0x41 .. 0x60]- ephSecret =- case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize X25519 ephemeral secret key: " ++ show err)- Right sk -> sk- ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- recipientPub =- case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of- Left err -> error ("failed to initialize X25519 recipient public key: " ++ show err)- Right pk -> pk- sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString- kek = deriveX25519KekForTest ephPublicRaw recipientPublicRaw sharedSecret- sessionKey = SessionKey (B.replicate 32 0x6f)- wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)- eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId (BL.replicate 8 0))- X25519- (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])))- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "conduitDecryptWithReport wildcard diagnostics"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- wrongKeyInfo =- PKESKRecipientKey {pkeskRecipientPKPayload = Just wrongPKP, pkeskRecipientSKey = wrongSKey}- correctKeyInfo =- PKESKRecipientKey {pkeskRecipientPKPayload = Just recipientPKP, pkeskRecipientSKey = recipientSKey}- let resolver _rid _pka = pure [wrongKeyInfo, correctKeyInfo]- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- (report, decrypted) <-- DC.runConduitRes $- CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- DC.fuseBoth- (DCD.conduitDecryptWithReport- DCD.DecryptOptions- { DCD.decryptOptionsKeyResolution = DCD.DecryptWithUnwrapCandidatesCallback resolver- , DCD.decryptOptionsPolicy = lenientDecryptPolicy- , DCD.decryptOptionsPassphraseCallback = passphraseCallback- })- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "wildcard retries with report diagnostics should decrypt" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)- assertEqual "decrypt report should preserve outcome" DCD.DecryptClean (DCD.decryptReportOutcome report)- case DCD.decryptReportSessionKeyResolutions report of- [resolution] -> do- assertEqual "resolution path should be PKESK for wildcard key retries" DCD.DecryptResolvedViaPKESK (DCD.decryptSessionResolutionPath resolution)- let attempts = DCD.decryptSessionResolutionResolverAttempts resolution- assertBool- "report should contain wildcard resolver attempts"- (length attempts >= 2)- assertBool- "report should record typed previous failures in resolver attempt context"- (any- (\attempt ->- any- (\failure ->- DCD.pkeskAttemptFailureKind failure == DCD.PKESKAttemptUnwrapFailed &&- DCD.pkeskAttemptFailureKeyContext failure == Just (V4, X25519))- (DCD.pkeskResolverAttemptPreviousFailures attempt))- attempts)- assertBool- "report should capture ResolveWith key context for X25519"- (any- (\attempt ->- DCD.pkeskResolverAttemptAction attempt ==- DCD.ResolverAttemptResolveWith (Just (V4, X25519)))- attempts)- other ->- assertFailure- ("Expected exactly one session-key resolution report, got " ++ show (length other))--testConduitDecryptSEIPDv2RejectsPKESKX25519V3WrongEphemeralLength :: Assertion-testConduitDecryptSEIPDv2RejectsPKESKX25519V3WrongEphemeralLength = do- let recipientSecretRaw = B.pack [0x21 .. 0x40]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize X25519 recipient secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- recipientPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip recipientPublicRaw))))- recipientSKey = X25519PrivateKey recipientSecretRaw- ephSecretRaw = B.pack [0x41 .. 0x60]- ephSecret =- case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize X25519 ephemeral secret key: " ++ show err)- Right sk -> sk- ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- recipientPub =- case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of- Left err -> error ("failed to initialize X25519 recipient public key: " ++ show err)- Right pk -> pk- sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString- kek = deriveX25519KekForTest ephPublicRaw recipientPublicRaw sharedSecret- sessionKey = SessionKey (B.replicate 32 0x6a)- wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)- eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession- overlongEphemeral = ephPublicRaw <> "\x00\x01"- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId "\x12\x34\x56\x78\x90\xab\xcd\xef")- X25519- (MPI (os2ip overlongEphemeral) :| [MPI (os2ip eskWithAlgo)])))- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk x25519 v3 wrong ephemeral length"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipientPKP- , pkeskRecipientSKey = recipientSKey- }))- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- result <-- catch- (Right <$>- DC.runConduitRes- (CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume))- (\e -> pure (Left (show (e :: SomeException))))- case result of- Left err ->- assertBool- "PKESKv3 X25519 unwrap should reject non-32-byte ephemeral values"- ("invalid X25519 ephemeral public key length/prefix" `isInfixOf` err)- Right packets ->- assertFailure- ("Expected X25519 ephemeral-length validation failure, got: " ++ show packets)--testConduitDecryptSEIPDv2FallsBackFromArgon2SKESKToPKESKv3X25519 :: Assertion-testConduitDecryptSEIPDv2FallsBackFromArgon2SKESKToPKESKv3X25519 = do- let recipientSecretRaw = B.pack [0x21 .. 0x40]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize X25519 recipient secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- recipientPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip recipientPublicRaw))))- recipientSKey = X25519PrivateKey recipientSecretRaw- ephSecretRaw = B.pack [0x41 .. 0x60]- ephSecret =- case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize X25519 ephemeral secret key: " ++ show err)- Right sk -> sk- ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- recipientPub =- case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of- Left err -> error ("failed to initialize X25519 recipient public key: " ++ show err)- Right pk -> pk- sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString- kek = deriveX25519KekForTest ephPublicRaw recipientPublicRaw sharedSecret- sessionKeyBytes = B.replicate 32 0x6a- sessionKey = SessionKey sessionKeyBytes- wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek sessionKeyBytes- eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId "\x12\x34\x56\x78\x90\xab\xcd\xef")- X25519- (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])))- skeskS2K = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15- skeskPassphrase = "correct horse battery staple"- wrongPassphrase = "wrong passphrase"- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "skesk->pkesk fallback decrypt path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipientPKP- , pkeskRecipientSKey = recipientSKey- }))- passphraseCallback _ = pure wrongPassphrase- keyLen <-- case keySize AES256 of- Left err -> assertFailure ("keySize failed for AES256: " ++ show err) >> fail "keySize failed"- Right n -> pure n- skeskKek <-- case string2Key skeskS2K keyLen skeskPassphrase of- Left err -> assertFailure ("string2Key failed for Argon2 SKESK: " ++ renderS2KError err) >> fail "string2Key failed"- Right x -> pure x- skeskEncryptedEsk <-- case withSymmetricCipher AES256 skeskKek $ \cipher ->- paddedCfbEncrypt- cipher- (B.replicate (blockSize cipher) 0)- (B.singleton (fromFVal AES256) <> sessionKeyBytes) of- Left err -> assertFailure ("encrypting Argon2 SKESK ESK failed: " ++ show err) >> fail "encrypting SKESK ESK failed"- Right x -> pure x- let skesk =- SKESKPkt- (SKESKPayloadV4Packet- (SKESKPayloadV4 AES256 skeskS2K (Just (BL.fromStrict skeskEncryptedEsk))))- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList- [ skesk- , pkesk- , SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))- ] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual- "conduitDecrypt should fall back from failing Argon2 SKESK to PKESKv3 X25519"- payload- gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)--testConduitDecryptSEIPDv2FallsBackToEarlierArgon2SKESK :: Assertion-testConduitDecryptSEIPDv2FallsBackToEarlierArgon2SKESK = do- let skeskS2K = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15- passphrase = "password"- sessionKeyBytes = B.replicate 32 0x7b- sessionKey = SessionKey sessionKeyBytes- badLatestEsk = BL.fromStrict (B.pack [0x00])- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "earlier argon2 skesk fallback decrypt path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure passphrase- keyLen <-- case keySize AES256 of- Left err -> assertFailure ("keySize failed for AES256: " ++ show err) >> fail "keySize failed"- Right n -> pure n- skeskKek <-- case string2Key skeskS2K keyLen passphrase of- Left err -> assertFailure ("string2Key failed for Argon2 SKESK: " ++ renderS2KError err) >> fail "string2Key failed"- Right x -> pure x- validEsk <-- case withSymmetricCipher AES256 skeskKek $ \cipher ->- paddedCfbEncrypt- cipher- (B.replicate (blockSize cipher) 0)- (B.singleton (fromFVal AES256) <> sessionKeyBytes) of- Left err -> assertFailure ("encrypting Argon2 SKESK ESK failed: " ++ show err) >> fail "encrypting SKESK ESK failed"- Right x -> pure x- let earlierValidSKESK =- SKESKPkt- (SKESKPayloadV4Packet- (SKESKPayloadV4 AES256 skeskS2K (Just (BL.fromStrict validEsk))))- latestUnusableSKESK =- SKESKPkt- (SKESKPayloadV4Packet- (SKESKPayloadV4 AES256 skeskS2K (Just badLatestEsk)))- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList- [ earlierValidSKESK- , latestUnusableSKESK- , SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))- ] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy (\_ -> pure Nothing) passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual- "conduitDecrypt should retry earlier Argon2 SKESK when latest SKESK is unusable"- payload- gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)--testConduitDecryptSEIPDv2RejectsECDHNonTable30Params :: Assertion-testConduitDecryptSEIPDv2RejectsECDHNonTable30Params = do- let (recipientPub, recipientPriv) = syntheticNISTP256Low- (ephPub, ephPriv) = syntheticNISTP256High- recipientPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- ECDH- (ECDHPubKey (ECDSAPubKey (ECDSA_PublicKey recipientPub)) SHA256 AES256)- recipientSKey = ECDHPrivateKey (ECDSA_PrivateKey recipientPriv)- sessionKey = B.replicate 32 0x2c- encodedSession =- B.singleton (fromFVal AES256) <>- sessionKey <>- encodeChecksum16 sessionKey <>- B.replicate 5 0- sharedSecret =- BA.convert- (ECCDH.getShared- (ECDSA.public_curve recipientPub)- (ECDSA.private_d ephPriv)- (ECDSA.public_q recipientPub)) :: B.ByteString- kdfParam =- buildECDHKDFParamForTest recipientPKP ECDH (ECDSA.public_curve recipientPub) SHA256 AES256- kek = deriveECDHKekForTest SHA256 AES256 sharedSecret kdfParam- wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession- ephPointBytes =- maybe- (error "failed to encode ephemeral point")- id- (point2MBS (ECDSA.public_q ephPub))- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId "\x99\x88\x77\x66\x55\x44\x33\x22")- ECDH- (MPI (os2ip ephPointBytes) :| [MPI (os2ip wrappedSession)])))- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk ecdh unwrap policy failure path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipientPKP- , pkeskRecipientSKey = recipientSKey- }))- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt (SessionKey sessionKey) (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- result <-- catch- (Right <$>- DC.runConduitRes- (CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume))- (\e -> pure (Left (show (e :: SomeException))))- case result of- Left err ->- assertBool- "ECDH unwrap should reject non-Table-30 parameters"- ("Table 30 policy violation" `isInfixOf` err)- Right packets ->- assertFailure- ("Expected Table 30 policy failure, got decrypted packets: " ++ show packets)--testConduitDecryptSEIPDv2AllowsV4Curve25519LegacyRFC6637AcceptedParams :: Assertion-testConduitDecryptSEIPDv2AllowsV4Curve25519LegacyRFC6637AcceptedParams = do- let recipientSecretRaw = B.pack [1 .. 32]- ephSecretRaw = B.pack [101 .. 132]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize Curve25519Legacy recipient secret key: " ++ show err)- Right sk -> sk- ephSecret =- case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize Curve25519Legacy ephemeral secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- recipientPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- ECDH- (ECDHPubKey- (EdDSAPubKey Ed25519 (PrefixedNativeEPoint (EPoint (os2ip (B.cons 0x40 recipientPublicRaw)))))- SHA256- AES256)- recipientSKey =- ECDHPrivateKey- (ECDSA_PrivateKey- (ECDSA.PrivateKey (ECCT.getCurveByName ECCT.SEC_p256r1) (os2ip recipientSecretRaw)))- sessionKey = B.replicate 32 0x3d- encodedSession =- B.singleton (fromFVal AES256) <>- sessionKey <>- encodeChecksum16 sessionKey <>- B.replicate 5 0- sharedSecret = BA.convert (C25519.dh (C25519.toPublic ephSecret) recipientSecret) :: B.ByteString- kdfParam = buildCurve25519LegacyKdfParamForTest recipientPKP ECDH SHA256 AES256- kek = deriveECDHKekForTest SHA256 AES256 sharedSecret kdfParam- wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId "\x99\x88\x77\x66\x55\x44\x33\x22")- ECDH- (MPI (os2ip ephPublicRaw) :| [MPI (os2ip wrappedSession)])))- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk ecdh v4 curve25519legacy accepted-combo path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipientPKP- , pkeskRecipientSKey = recipientSKey- }))- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt (SessionKey sessionKey) (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual- "v4 Curve25519Legacy ECDH should allow RFC6637-accepted parameters"- payload- gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)--testConduitDecryptSEIPDv2AllowsV4Curve25519LegacyWithTruncatedWrappedMPI :: Assertion-testConduitDecryptSEIPDv2AllowsV4Curve25519LegacyWithTruncatedWrappedMPI = do- let recipientSecretRaw = B.pack [1 .. 32]- ephSecretRaw = B.pack [101 .. 132]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize Curve25519Legacy recipient secret key: " ++ show err)- Right sk -> sk- ephSecret =- case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize Curve25519Legacy ephemeral secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- recipientPKP =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- ECDH- (ECDHPubKey- (EdDSAPubKey Ed25519 (PrefixedNativeEPoint (EPoint (os2ip (B.cons 0x40 recipientPublicRaw)))))- SHA256- AES256)- recipientSKey =- ECDHPrivateKey- (ECDSA_PrivateKey- (ECDSA.PrivateKey (ECCT.getCurveByName ECCT.SEC_p256r1) (os2ip recipientSecretRaw)))- sharedSecret = BA.convert (C25519.dh (C25519.toPublic ephSecret) recipientSecret) :: B.ByteString- kdfParam = buildCurve25519LegacyKdfParamForTest recipientPKP ECDH SHA256 AES256- kek = deriveECDHKekForTest SHA256 AES256 sharedSecret kdfParam- candidate =- find- (\(_, wrappedSession) -> not (B.null wrappedSession) && B.head wrappedSession == 0x00)- [ let sessionKeyBytes =- B.pack- [ fromIntegral ((seed + offset) `mod` 256)- | offset <- [0 .. 31]- ]- encodedSession =- B.singleton (fromFVal AES256) <>- sessionKeyBytes <>- encodeChecksum16 sessionKeyBytes <>- B.replicate 5 0- wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession- in (SessionKey sessionKeyBytes, wrappedSession)- | seed <- [0 .. 4095 :: Int]- ]- case candidate of- Nothing ->- assertFailure- "failed to find deterministic Curve25519Legacy wrapped session key with leading zero"- Just (sessionKey, wrappedSession) -> do- let pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId "\x99\x88\x77\x66\x55\x44\x33\x22")- ECDH- (MPI (os2ip ephPublicRaw) :| [MPI (os2ip wrappedSession)])))- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk ecdh v4 curve25519legacy truncated wrapped-mpi path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipientPKP- , pkeskRecipientSKey = recipientSKey- }))- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt sessionKey (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList- [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual- "v4 Curve25519Legacy ECDH should unwrap after wrapped-MPI leading-zero truncation"- payload- gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)--testConduitDecryptSEIPDv2RejectsV6Curve25519LegacyNonTable30Params :: Assertion-testConduitDecryptSEIPDv2RejectsV6Curve25519LegacyNonTable30Params = do- let recipientSecretRaw = B.pack [1 .. 32]- ephSecretRaw = B.pack [101 .. 132]- recipientSecret =- case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize Curve25519Legacy recipient secret key: " ++ show err)- Right sk -> sk- ephSecret =- case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize Curve25519Legacy ephemeral secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString- ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString- recipientPKP =- PKPayload- V6- (ThirtyTwoBitTimeStamp 0)- 0- ECDH- (ECDHPubKey- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip recipientPublicRaw))))- SHA512- AES256)- recipientSKey =- ECDHPrivateKey- (ECDSA_PrivateKey- (ECDSA.PrivateKey (ECCT.getCurveByName ECCT.SEC_p256r1) (os2ip recipientSecretRaw)))- sessionKey = B.replicate 32 0x3d- encodedSession =- B.singleton (fromFVal AES256) <>- sessionKey <>- encodeChecksum16 sessionKey <>- B.replicate 5 0- sharedSecret = BA.convert (C25519.dh (C25519.toPublic ephSecret) recipientSecret) :: B.ByteString- kdfParam = buildCurve25519LegacyKdfParamForTest recipientPKP ECDH SHA512 AES256- kek = deriveECDHKekForTest SHA512 AES256 sharedSecret kdfParam- wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession- pkesk =- PKESKPkt- (PKESKPayloadV3Packet- (PKESKPayloadV3- 3- (EightOctetKeyId "\x99\x88\x77\x66\x55\x44\x33\x22")- ECDH- (MPI (os2ip ephPublicRaw) :| [MPI (os2ip wrappedSession)])))- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk ecdh v6 curve25519legacy strict path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipientPKP- , pkeskRecipientSKey = recipientSKey- }))- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt (SessionKey sessionKey) (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- result <-- catch- (Right <$>- DC.runConduitRes- (CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithDecryptPolicy lenientDecryptPolicy keyContextCallback passphraseCallback DC..|- CL.consume))- (\e -> pure (Left (show (e :: SomeException))))- case result of- Left err ->- assertBool- "v6 Curve25519Legacy ECDH should remain strict"- ("Table 30 policy violation" `isInfixOf` err)- Right packets ->- assertFailure- ("Expected Table 30 policy failure, got decrypted packets: " ++ show packets)--testConduitDecryptSEIPDv2WithPKESKX448Unwrap :: Assertion-testConduitDecryptSEIPDv2WithPKESKX448Unwrap = do- let recipientSecretRaw = B.pack [1 .. 56]- ephSecretRaw = B.pack [57 .. 112]- sessionKey = B.replicate 32 0x4d- encodedSession =- B.singleton (fromFVal AES256) <>- sessionKey <>- encodeChecksum16 sessionKey <>- B.replicate 5 0- recipientSecret =- case CE.eitherCryptoError (C448.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize X448 recipient secret key: " ++ show err)- Right sk -> sk- ephSecret =- case CE.eitherCryptoError (C448.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize X448 ephemeral secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C448.toPublic recipientSecret) :: B.ByteString- ephPublicRaw = BA.convert (C448.toPublic ephSecret) :: B.ByteString- sharedSecret = BA.convert (C448.dh (C448.toPublic recipientSecret) ephSecret) :: B.ByteString- kek = deriveX448KekForTest ephPublicRaw recipientPublicRaw sharedSecret- wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession- esk =- ephPublicRaw <>- B.singleton (fromIntegral (B.length wrappedSession)) <>- wrappedSession- recipientPKP =- PKPayload- V6- (ThirtyTwoBitTimeStamp 0)- 0- X448- (EdDSAPubKey Ed448 (NativeEPoint (EPoint (os2ip recipientPublicRaw))))- recipientRid = unFingerprint (fingerprint recipientPKP)- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk x448 unwrap decrypt path"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipientPKP- , pkeskRecipientSKey = X448PrivateKey recipientSecretRaw- }))- pkesk =- PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 recipientRid X448 (BL.fromStrict esk)))- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt (SessionKey sessionKey) (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- decrypted <-- DC.runConduitRes $- CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithPKESKContext keyContextCallback passphraseCallback DC..|- CL.consume- case decrypted of- [LiteralDataPkt _ _ _ gotPayload] ->- assertEqual "PKESK X448 unwrap decrypt payload" payload gotPayload- other ->- assertFailure ("Expected one decrypted literal packet, got " ++ show other)--testConduitDecryptSEIPDv2RejectsPKESKX448WrongEphemeralLength :: Assertion-testConduitDecryptSEIPDv2RejectsPKESKX448WrongEphemeralLength = do- let recipientSecretRaw = B.pack [1 .. 56]- ephSecretRaw = B.pack [57 .. 112]- sessionKey = B.replicate 32 0x4d- encodedSession =- B.singleton (fromFVal AES256) <>- sessionKey <>- encodeChecksum16 sessionKey <>- B.replicate 5 0- recipientSecret =- case CE.eitherCryptoError (C448.secretKey recipientSecretRaw) of- Left err -> error ("failed to initialize X448 recipient secret key: " ++ show err)- Right sk -> sk- ephSecret =- case CE.eitherCryptoError (C448.secretKey ephSecretRaw) of- Left err -> error ("failed to initialize X448 ephemeral secret key: " ++ show err)- Right sk -> sk- recipientPublicRaw = BA.convert (C448.toPublic recipientSecret) :: B.ByteString- ephPublicRaw = BA.convert (C448.toPublic ephSecret) :: B.ByteString- shortEphemeral = B.tail ephPublicRaw- sharedSecret = BA.convert (C448.dh (C448.toPublic recipientSecret) ephSecret) :: B.ByteString- kek = deriveX448KekForTest ephPublicRaw recipientPublicRaw sharedSecret- wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession- esk =- shortEphemeral <>- B.singleton (fromIntegral (B.length wrappedSession)) <>- wrappedSession- recipientPKP =- PKPayload- V6- (ThirtyTwoBitTimeStamp 0)- 0- X448- (EdDSAPubKey Ed448 (NativeEPoint (EPoint (os2ip recipientPublicRaw))))- recipientRid = unFingerprint (fingerprint recipientPKP)- salt = Salt (B.pack [0x00 .. 0x1f])- payload = "pkesk x448 wrong ephemeral length"- literalBlock =- Block [LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) payload]- passphraseCallback _ = pure BL.empty- keyContextCallback _ =- pure- (Just- (PKESKRecipientKey- { pkeskRecipientPKPayload = Just recipientPKP- , pkeskRecipientSKey = X448PrivateKey recipientSecretRaw- }))- pkesk =- PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 recipientRid X448 (BL.fromStrict esk)))- ciphertext <-- case encryptSEIPDv2Payload AES256 OCB 6 salt (SessionKey sessionKey) (BL.toStrict (runPut (put literalBlock))) of- Left err -> assertFailure ("encryptSEIPDv2Payload failed: " ++ err) >> pure mempty- Right ct -> pure ct- result <-- catch- (Right <$>- DC.runConduitRes- (CL.sourceList [pkesk, SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))] DC..|- conduitDecryptWithPKESKContext keyContextCallback passphraseCallback DC..|- CL.consume))- (\e -> pure (Left (show (e :: SomeException))))- case result of- Left err ->- assertBool- "PKESKv6 X448 unwrap should reject non-56-byte ephemeral values"- ("expected 56-octet ephemeral value" `isInfixOf` err)- Right packets ->- assertFailure- ("Expected X448 ephemeral-length validation failure, got: " ++ show packets)--testDecodeOpenPGPEncodedSessionKeyRejectsTooShort :: Assertion-testDecodeOpenPGPEncodedSessionKeyRejectsTooShort =- assertEqual- "encoded session key should reject too-short payloads"- (Left EncodedSessionKeyTooShort)- (decodeOpenPGPEncodedSessionKey (B.pack [0x09, 0x01]))--testDecodeOpenPGPEncodedSessionKeyRejectsLengthMismatch :: Assertion-testDecodeOpenPGPEncodedSessionKeyRejectsLengthMismatch =- assertEqual- "encoded session key should reject algorithm/key-length mismatches"- (Left (EncodedSessionKeyLengthMismatch AES128 16 3))- (decodeOpenPGPEncodedSessionKey (B.pack [fromFVal AES128, 0x01, 0x02, 0x03]))--testDecodeOpenPGPEncodedSessionKeyRejectsChecksumMismatch :: Assertion-testDecodeOpenPGPEncodedSessionKeyRejectsChecksumMismatch =- assertEqual- "encoded session key should reject checksum mismatches"- (Left EncodedSessionKeyChecksumMismatch)- (decodeOpenPGPEncodedSessionKey (B.pack ([fromFVal AES128] <> replicate 16 0x00 <> [0x00, 0x01])))--testSKESKRejectsUnknownVersion :: Assertion-testSKESKRejectsUnknownVersion = do- let s2k = Simple SHA256- pkt = SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 AES128 s2k Nothing))- encoded = BL.toStrict (runPut (put pkt))- malformed = BL.fromStrict (B.take 2 encoded <> B.singleton 5 <> B.drop 3 encoded)- case runGet (get :: Get Pkt) malformed of- Right (BrokenPacketPkt errReason 3 _) ->- assertBool- ("expected unsupported SKESK version error, got: " ++ errReason)- ("unsupported SKESK packet version" `isInfixOf` errReason)- Right other ->- assertFailure ("unknown SKESK version should not parse successfully: " ++ show other)- Left err ->- assertFailure ("unknown SKESK version should be reported as a broken packet: " ++ err)--testSKESK4EncryptedSessionKeyRejectsSimpleS2K :: Assertion-testSKESK4EncryptedSessionKeyRejectsSimpleS2K = do- let pkt =- SKESKPkt- (SKESKPayloadV4Packet- (SKESKPayloadV4 AES128 (Simple SHA256) (Just (BL.pack [0x01, 0x02, 0x03]))))- encoded = runPut (put pkt)- case runGet (get :: Get Pkt) encoded of- Right (BrokenPacketPkt errReason 3 _) ->- assertBool- ("expected Simple S2K rejection, got: " ++ errReason)- ("must not use Simple S2K" `isInfixOf` errReason)- Right other ->- assertFailure- ("Simple-S2K v4 SKESK with encrypted session key should not parse successfully: " ++- show other)- Left err ->- assertFailure- ("Simple-S2K v4 SKESK with encrypted session key should be reported as a broken packet: " ++- err)--testSKESK4Argon2EncryptedSessionKeyRoundTripAcrossAES :: Assertion-testSKESK4Argon2EncryptedSessionKeyRoundTripAcrossAES =- mapM_ assertRoundTrip [AES128, AES192, AES256]- where- passphrase = "password"- s2k = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15- assertRoundTrip sa = do- keyLen <-- case keySize sa of- Left err -> assertFailure ("keySize failed for " ++ show sa ++ ": " ++ show err) >> fail "keySize failed"- Right n -> pure n- kek <-- case string2Key s2k keyLen passphrase of- Left err ->- assertFailure ("string2Key failed for " ++ show sa ++ ": " ++ renderS2KError err) >>- fail "string2Key failed"- Right x -> pure x- let sessionKey = B.pack (take keyLen (cycle [0x11, 0x22, 0x33, 0x44]))- encodedSessionKey = B.singleton (fromFVal sa) <> sessionKey- encryptedEsk <-- case withSymmetricCipher sa kek $ \cipher ->- paddedCfbEncrypt- cipher- (B.replicate (blockSize cipher) 0)- encodedSessionKey of- Left err ->- assertFailure ("encrypting SKESK v4 ESK failed for " ++ show sa ++ ": " ++ show err) >>- fail "encrypting SKESK v4 ESK failed"- Right x -> pure x- let skesk = SKESK4Packet sa s2k (Just (BL.fromStrict encryptedEsk))- case skesk2SessionKey skesk passphrase of- Right (decodedAlgo, decodedSessionKey) -> do- assertEqual ("decoded SKESK v4 encrypted ESK algorithm for " ++ show sa) sa decodedAlgo- assertEqual ("decoded SKESK v4 encrypted ESK session key for " ++ show sa) sessionKey decodedSessionKey- Left err ->- assertFailure- ("decoding SKESK v4 encrypted ESK failed for " ++- show sa ++ ": " ++ renderS2KError err)--testSKESK4Argon2EncryptedSessionKeyRejectsTrailingChecksum :: Assertion-testSKESK4Argon2EncryptedSessionKeyRejectsTrailingChecksum = do- let sa = AES128- passphrase = "password"- s2k = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15- sessionKey = B.pack (take 16 (cycle [0xaa, 0xbb, 0xcc, 0xdd]))- keyLen <-- case keySize sa of- Left err -> assertFailure ("keySize failed: " ++ show err) >> fail "keySize failed"- Right n -> pure n- kek <-- case string2Key s2k keyLen passphrase of- Left err -> assertFailure ("string2Key failed: " ++ renderS2KError err) >> fail "string2Key failed"- Right x -> pure x- let encodedWithChecksum =- B.singleton (fromFVal sa) <> sessionKey <> encodeChecksum16 sessionKey- encryptedEsk <-- case withSymmetricCipher sa kek $ \cipher ->- paddedCfbEncrypt- cipher- (B.replicate (blockSize cipher) 0)- encodedWithChecksum of- Left err ->- assertFailure ("encrypting malformed SKESK v4 ESK failed: " ++ show err) >>- fail "encrypting malformed SKESK v4 ESK failed"- Right x -> pure x- let skesk = SKESK4Packet sa s2k (Just (BL.fromStrict encryptedEsk))- case skesk2SessionKey skesk passphrase of- Left (S2KEncryptedSessionKeyDecodeError (EncodedSessionKeyLengthMismatch AES128 16 18)) -> pure ()- Left err ->- assertFailure- ("expected SKESK v4 trailing-checksum rejection, got: " ++- renderS2KError err)- Right _ ->- assertFailure "expected SKESK v4 trailing-checksum rejection, but decode succeeded"--testEncryptPassphraseWithPolicyForceV4Interop :: Assertion-testEncryptPassphraseWithPolicyForceV4Interop = do- let request =- PassphraseEncryptRequest- { passphraseEncryptVersionPolicy = PassphraseSKESKForceV4Interop- , passphraseEncryptSymmetricAlgorithm = AES128- , passphraseEncryptS2K = Simple SHA256- , passphraseEncryptPassphrase = "password"- , passphraseEncryptPayload = "hello"- , passphraseEncryptSEIPDv1IVOverride = Just (IV (B.replicate 16 0x22))- , passphraseEncryptSEIPDv2AEADOverride = Nothing- , passphraseEncryptSEIPDv2ChunkSizeOverride = Nothing- , passphraseEncryptSEIPDv2SaltOverride = Nothing- }- result <- encryptPassphraseWithPolicy request- case result of- Left err ->- assertFailure ("Expected passphrase SKESK force-v4 encryption to succeed, got: " ++ err)- Right (SKESKPkt (SKESKPayloadV4Packet _):SymEncIntegrityProtectedDataPkt (SEIPD1 _ _):_) ->- pure ()- Right packets ->- assertFailure- ("Expected SKESKv4 + SEIPDv1 packet sequence, got: " ++ show packets)--testEncryptPassphraseWithPolicyPreferV6 :: Assertion-testEncryptPassphraseWithPolicyPreferV6 = do- let request =- PassphraseEncryptRequest- { passphraseEncryptVersionPolicy = PassphraseSKESKPreferV6- , passphraseEncryptSymmetricAlgorithm = AES128- , passphraseEncryptS2K = Simple SHA256- , passphraseEncryptPassphrase = "password"- , passphraseEncryptPayload = "hello"- , passphraseEncryptSEIPDv1IVOverride = Nothing- , passphraseEncryptSEIPDv2AEADOverride = Just OCB- , passphraseEncryptSEIPDv2ChunkSizeOverride = Just 6- , passphraseEncryptSEIPDv2SaltOverride = Just (Salt (B.replicate 32 0x44))- }- result <- encryptPassphraseWithPolicy request- case result of- Left err ->- assertFailure ("Expected passphrase SKESK prefer-v6 encryption to succeed, got: " ++ err)- Right (SKESKPkt (SKESKPayloadV6Packet _):SymEncIntegrityProtectedDataPkt (SEIPD2 _ _ _ _ _):_) ->- pure ()- Right packets ->- assertFailure- ("Expected SKESKv6 + SEIPDv2 packet sequence, got: " ++ show packets)--testArgon2S2KVector :: Assertion-testArgon2S2KVector = do- let s2k = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15- pass = BLC8.pack "password"- derivedKeyResult = string2Key s2k 16 pass- derivedKey <-- case derivedKeyResult of- Left err ->- assertFailure ("Argon2 S2K key derivation failed: " ++ renderS2KError err) >>- fail "Argon2 key derivation failed"- Right x -> pure x- let- hex = map toUpper . BLC8.unpack . B16L.encode . BL.fromStrict $ derivedKey- assertEqual- "Argon2 S2K key derivation vector"- "A3BDFE3814D790F1F366ABA6954EB386"- hex--testArgon2S2KOrdTotal :: Assertion-testArgon2S2KOrdTotal = do- let argon2A = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15- argon2B = Argon2 (Salt16 (B.pack [0x01 .. 0x10])) 1 4 15- assertEqual- "Argon2 S2K compares after simple S2K"- GT- (compare argon2A (Simple SHA256))- assertEqual- "Argon2 S2K compares before unknown S2K types"- LT- (compare argon2A (OtherS2K 101 BL.empty))- assertEqual- "Argon2 S2K constructor compares lexicographically by fields"- LT- (compare argon2A argon2B)+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}++module Tests.Encryption (encryptionAndCompressionTests) where++import qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA+import Codec.Encryption.OpenPGP.ASCIIArmor.Types (Armor (..))+import Control.Exception.Base (SomeException, catch, try)+import Control.Monad (void)+import Control.Monad.Trans.Resource (ResourceT)+import qualified Crypto.Error as CE+import Crypto.Number.ModArithmetic (inverse)+import Crypto.Number.Serialize (i2osp, os2ip)+import qualified Crypto.PubKey.Curve25519 as C25519+import qualified Crypto.PubKey.Curve448 as C448+import qualified Crypto.PubKey.ECC.DH as ECCDH+import qualified Crypto.PubKey.ECC.ECDSA as ECDSA+import Crypto.PubKey.ECC.Prim (pointBaseMul)+import qualified Crypto.PubKey.ECC.Types as ECCT+import qualified Crypto.PubKey.RSA as RSA+import qualified Crypto.PubKey.RSA.PKCS15 as P15+import Data.Binary (get, put)+import Data.Binary.Get+ ( Get+ )+import Data.Binary.Put (putWord16be, runPut)+import qualified Data.ByteArray as BA+import qualified Data.ByteString as B+import qualified Data.ByteString.Base16.Lazy as B16L+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Lazy.Char8 as BLC8+import Data.Char (toUpper)+import Data.Conduit (fuseBoth)+import qualified Data.Conduit as DC+import qualified Data.Conduit.Binary as CB+import qualified Data.Conduit.List as CL+import Data.Conduit.Serialization.Binary (conduitGet)+import Data.List (find, isInfixOf)+import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NE+import Data.Maybe (listToMaybe)+import qualified Data.Set as Set+import Data.Word (Word16)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit+ ( Assertion+ , assertBool+ , assertEqual+ , assertFailure+ , testCase+ )++import Codec.Encryption.OpenPGP.BlockCipher+ ( keySize+ , withSymmetricCipher+ )+import Codec.Encryption.OpenPGP.CFB+import Codec.Encryption.OpenPGP.Compression+ ( CompressionError (..)+ , compressPkts+ , decompressPkt+ )+import Codec.Encryption.OpenPGP.Encrypt+ ( EncryptCompatibilityProfile (..)+ , PKESKEncryptError+ ( InvalidRecipientIdentifier+ , InvalidRecipientKeyMaterial+ , PayloadBuildFailure+ , RecipientCapabilitySelectionFailure+ , RecipientKdfFailure+ , UnsupportedRecipientAlgorithm+ )+ , PKESKVersionPolicy (..)+ , PassphraseEncryptRequest (..)+ , PassphraseSKESKVersionPolicy (..)+ , RecipientCapabilities (..)+ , RecipientCapabilityError+ ( RecipientCapabilityMissingSEIPDv1Support+ , RecipientCapabilityNoCommonAEADAlgorithms+ , RecipientCapabilityNoCommonSymmetricAlgorithms+ , RecipientCapabilityNoEncryptableKeyMaterialInTK+ )+ , RecipientCapabilityNegotiationMode (..)+ , RecipientEncryptRequest (..)+ , RecipientEncryptRequestOverrides (..)+ , RecipientEncryptResult (..)+ , RecipientEncryptionTarget (..)+ , RecipientEncryptionTargetRejected (..)+ , RecipientEncryptionTargetsReport (..)+ , RecipientPKESKVersionStrategy (..)+ , RecipientPayloadShape (..)+ , RecipientTargetRejectionReason (..)+ , RecipientTargetSelectionPolicy (..)+ , buildPKESKPayloadForRecipient+ , buildPKESKv3PayloadForRecipient+ , canonicalizePKESKRecipientId+ , defaultRecipientPayloadShape+ , encryptForRecipients+ , encryptForRecipientsLegacy+ , encryptForRecipientsWithCapabilityNegotiation+ , encryptPassphraseWithPolicy+ , encryptSEIPDv2Payload+ , pkeskSessionAlgorithm+ , pkeskV3SessionMaterial+ , recipientCapabilitiesFromSubpacketPayloads+ , recipientEncryptionTarget+ , recipientEncryptionTargetFromTK+ , recipientEncryptionTargetFromTKAtTimestamp+ , recipientEncryptionTargetFromTKAtTimestampWithPolicy+ , recipientEncryptionTargetWithCapabilities+ , recipientEncryptionTargetWithStrategy+ , recipientEncryptionTargetsFromTK+ , recipientEncryptionTargetsFromTKAtTimestamp+ , recipientEncryptionTargetsReportFromTKAtTimestamp+ , recipientVersionStrategyForProfile+ )+import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)+import Codec.Encryption.OpenPGP.Internal (point2MBS)+import Codec.Encryption.OpenPGP.Internal.HOBlockCipher+ ( HOBlockCipher (..)+ )+import Codec.Encryption.OpenPGP.Message+ ( SessionMaterialExposure (..)+ , encryptedPayloadBytes+ , mkClearPayload+ , mkPassphrase+ )+import Codec.Encryption.OpenPGP.Policy+ ( OpenPGPPolicy (..)+ , OpenPGPRFC (..)+ , defaultDecryptPolicy+ , defaultPolicy+ , lenientDecryptPolicy+ , policyForRFC+ )+import Codec.Encryption.OpenPGP.S2K+ ( EncodedSessionKeyError (..)+ , S2KError (..)+ , decodeOpenPGPEncodedSessionKey+ , renderS2KError+ , skesk2SessionKey+ , string2Key+ )+import Codec.Encryption.OpenPGP.SecretKey+ ( decryptPrivateKey+ , encryptPrivateKeyWithPolicyAndSaltAndIV+ )+import Codec.Encryption.OpenPGP.Serialize (parsePkts)+import Codec.Encryption.OpenPGP.Types+import Data.Conduit.OpenPGP.Compression (conduitCompress)+import Data.Conduit.OpenPGP.Decrypt+ ( DecryptKeyResolution (..)+ , DecryptOptions (..)+ , DecryptOutcome (..)+ , PKESKRecipientKey (..)+ )+import qualified Data.Conduit.OpenPGP.Decrypt as DCD+import Data.Conduit.OpenPGP.Keyring+ ( conduitToSomeTKsDroppingEither+ , conduitToSomeTKsEither+ , conduitToTKsDroppingEither+ , conduitToTKsEither+ )+import Tests.Common+ ( aesKeyWrapRFC3394ForTest+ , armorPayload+ , assertX25519EskShape+ , assertX448EskShape+ , buildCurve25519LegacyKdfParamForTest+ , buildECDHKDFParamForTest+ , cgp+ , collectSecretKeyInfos+ , conduitDecrypt+ , conduitDecryptChecked+ , conduitDecryptCheckedWithDecryptPolicy+ , conduitDecryptWithCandidatesCallbackAndPolicy+ , conduitDecryptWithDecryptPolicy+ , conduitDecryptWithPKESKContext+ , deriveECDHKekForTest+ , deriveX25519KekForTest+ , deriveX448KekForTest+ , doPkeyAndSkeyMatch+ , encodeChecksum16+ , encryptMessageDefault+ , fixturePath+ , forceVersionedRecipientIdentifier+ , isPrecedingESK+ , loadArmor+ , loadDeterministicEd25519Signer+ , loadSEIPDv2FixtureWithV4Secret+ , loadUnencryptedRsaSigner+ , mkPKESKSessionMaterialOrFail+ , prependUnusableLatestPKESK+ , readFixtureLazy+ , readFixturePackets+ , readFixtureStrict+ , readPKIPassphrase+ , reorderPrecedingPKESKs+ , runGet+ , selectRecipientKeyInfo+ , selectRecipientKeyInfoByRawRecipientId+ , setKeyTimestamp+ , setKeyVersion+ , signDirectKeyWithRSAExtrasAt+ , signSubkeyBindingWithRSAExtrasAt+ , signSubkeyRevocationWithRSAAt+ , testEncodeOpenPGPSessionMaterial+ , testParsedRSASecretKeyPKCS15DecryptNotMessageNotRecognized+ , testSEIPDv2ForV4KeyArmor+ , testSEIPDv2ThreeRecipientsArmor+ , testSEIPDv2TwoRecipientsArmor+ )++mkLegacySymmetricCase+ :: (String, Bool, FilePath, FilePath, BL.ByteString) -> TestTree+mkLegacySymmetricCase (label, expectSEIPDv1, encFile, passFile, cleartext) =+ testCase+ label+ ( testLegacySymmetricEncryption+ expectSEIPDv1+ encFile+ passFile+ cleartext+ )++legacySymmetricCases+ :: [(String, Bool, FilePath, FilePath, BL.ByteString)]+legacySymmetricCases =+ [+ ( "Symmetric Encryption simple S2K SHA1 3DES, no MDC"+ , False+ , "encryption-sym-3des-s2k0.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted S2K SHA1 3DES, no MDC"+ , False+ , "encryption-sym-3des.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption simple S2K SHA1 3DES"+ , True+ , "encryption-sym-3des-mdc-s2k0.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted S2K SHA1 3DES"+ , True+ , "encryption-sym-3des-mdc.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption simple S2K SHA1 CAST5, no MDC"+ , False+ , "encryption-sym-cast5-s2k0.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted S2K SHA1 CAST5, no MDC"+ , False+ , "encryption-sym-cast5.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption simple S2K SHA1 CAST5"+ , True+ , "encryption-sym-cast5-mdc-s2k0.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted S2K SHA1 CAST5"+ , True+ , "encryption-sym-cast5-mdc.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption simple S2K SHA1 Blowfish, no MDC"+ , False+ , "encryption-sym-blowfish-s2k0.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted S2K SHA1 Blowfish, no MDC"+ , False+ , "encryption-sym-blowfish.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption simple S2K SHA1 Blowfish"+ , True+ , "encryption-sym-blowfish-mdc-s2k0.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted S2K SHA1 Blowfish"+ , True+ , "encryption-sym-blowfish-mdc.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption simple S2K SHA1 AES128"+ , True+ , "encryption-sym-aes128-s2k0.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted S2K SHA1 AES128"+ , True+ , "encryption-sym-aes128.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption simple S2K SHA1 AES192"+ , True+ , "encryption-sym-aes192-s2k0.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted S2K SHA1 AES192"+ , True+ , "encryption-sym-aes192.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption simple S2K SHA1 AES256"+ , True+ , "encryption-sym-aes256-s2k0.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted S2K SHA1 AES256"+ , True+ , "encryption-sym-aes256.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted S2K SHA256 AES256"+ , True+ , "encryption-sym-aes256-sha256.pgp"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption simple S2K SHA1 Twofish"+ , True+ , "encryption-sym-twofish-s2k0.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted S2K SHA1 Twofish"+ , True+ , "encryption-sym-twofish.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption simple Camellia128"+ , True+ , "encryption-sym-camellia128-s2k0.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted Camellia128"+ , True+ , "encryption-sym-camellia128.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted Camellia192"+ , True+ , "encryption-sym-camellia192.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption iterated-salted Camellia256"+ , True+ , "encryption-sym-camellia256.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "Symmetric Encryption pgcrypto"+ , True+ , "encryption-sym-pgcrypto.pgp"+ , "pgcrypto-passphrase.txt"+ , "{\"t\":\"plus\"}"+ )+ ]++mkStrictSymmetricCase+ :: (String, FilePath, FilePath, BL.ByteString) -> TestTree+mkStrictSymmetricCase (label, encFile, passFile, cleartext) =+ testCase+ label+ (testSymmetricEncryption encFile passFile cleartext)++strictSymmetricCases+ :: [(String, FilePath, FilePath, BL.ByteString)]+strictSymmetricCases =+ [+ ( "strict policy AES128 iterated-salted S2K SEIPDv1"+ , "encryption-sym-aes128.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "strict policy AES192 iterated-salted S2K SEIPDv1"+ , "encryption-sym-aes192.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ,+ ( "strict policy AES256 iterated-salted S2K SEIPDv1"+ , "encryption-sym-aes256.gpg"+ , "symmetric-password.txt"+ , "test\n"+ )+ ]++mkSyntheticNISTP256+ :: B.ByteString -> (ECDSA.PublicKey, ECDSA.PrivateKey)+mkSyntheticNISTP256 bs =+ let curve = ECCT.getCurveByName ECCT.SEC_p256r1+ d = os2ip bs+ q = pointBaseMul curve d+ pub = ECDSA.PublicKey curve q+ priv = ECDSA.PrivateKey curve d+ in (pub, priv)++syntheticNISTP256Low+ , syntheticNISTP256High+ :: (ECDSA.PublicKey, ECDSA.PrivateKey)+syntheticNISTP256Low = mkSyntheticNISTP256 $ B.pack [1 .. 32]+syntheticNISTP256High = mkSyntheticNISTP256 $ B.pack [101 .. 132]++encryptionAndCompressionTests :: TestTree+encryptionAndCompressionTests =+ testGroup+ "Encryption and compression"+ [ testGroup+ "Compression group"+ [ testCase+ "compressedsig.gpg"+ (testCompression "compressedsig.gpg")+ , testCase+ "compressedsig-zlib.gpg"+ (testCompression "compressedsig-zlib.gpg")+ , testCase+ "compressedsig-bzip2.gpg"+ (testCompression "compressedsig-bzip2.gpg")+ , testCase+ "decompressPkt classifies empty ZIP payload"+ ( assertEqual+ "empty ZIP"+ (Left (EmptyCompressedPayload ZIP))+ (decompressPkt (CompressedDataPkt ZIP ""))+ )+ , testCase+ "decompressPkt classifies empty Uncompressed payload"+ ( assertEqual+ "empty Uncompressed"+ (Left (EmptyCompressedPayload Uncompressed))+ (decompressPkt (CompressedDataPkt Uncompressed ""))+ )+ , testCase+ "decompressPkt classifies marker-only ZIP payload"+ ( let markerPkt = MarkerPkt "\x50\x47\x50"+ compressed = compressPkts ZIP [markerPkt]+ in assertEqual+ "marker-only"+ (Left (MarkerOnlyPayload ZIP))+ (decompressPkt compressed)+ )+ , testCase+ "decompressPkt passes through non-compressed packets"+ ( let pkt = OtherPacketPkt 255 ""+ in assertEqual+ "passthrough"+ (Right [pkt])+ (decompressPkt pkt)+ )+ ]+ , testGroup+ "Conduit length group"+ [ testCase+ "conduitCompress (ZIP)"+ ( testConduitOutputLength+ "pubring.gpg"+ (cgp DC..| conduitCompress ZIP)+ 1+ )+ , testCase+ "conduitCompress (Zlib)"+ ( testConduitOutputLength+ "pubring.gpg"+ (cgp DC..| conduitCompress ZLIB)+ 1+ )+ , testCase+ "conduitCompress (BZip2)"+ ( testConduitOutputLength+ "pubring.gpg"+ (cgp DC..| conduitCompress BZip2)+ 1+ )+ , testCase+ "conduitToTKsDroppingEither"+ ( testConduitOutputLength+ "pubring.gpg"+ (cgp DC..| conduitToTKsDroppingEither)+ 4+ )+ , testCase+ "conduitToSomeTKsDroppingEither"+ ( testConduitOutputLength+ "pubring.gpg"+ (cgp DC..| conduitToSomeTKsDroppingEither)+ 4+ )+ , testCase+ "conduitToTKsEither reports parse failures"+ testConduitToTKsEitherReportsParseFailure+ , testCase+ "conduitToSomeTKsEither reports parse failures"+ testConduitToSomeTKsEitherReportsParseFailure+ , testCase+ "conduitToTKsDroppingEither reports parse failures"+ testConduitToTKsDroppingEitherReportsParseFailure+ , testCase+ "conduitToSomeTKsDroppingEither reports parse failures"+ testConduitToSomeTKsDroppingEitherReportsParseFailure+ ]+ , testGroup+ "Encrypted data"+ ( [ testCase+ "Legacy symmetric encryption fixture shape"+ testLegacyEncryptionFixtureShape+ , testCase+ "SEIPDv1 resync nonce/MDC roundtrip"+ testSEIPDv1ResyncNonceMdcRoundTrip+ ]+ ++ map mkLegacySymmetricCase legacySymmetricCases+ ++ map mkStrictSymmetricCase strictSymmetricCases+ ++ [ testCase+ "strict policy rejects SED (no MDC)"+ ( testStrictPolicyRejectsEncryption+ "encryption-sym-cast5.gpg"+ "symmetric-password.txt"+ "unauthenticated SED"+ )+ , testCase+ "strict policy rejects Simple S2K"+ ( testStrictPolicyRejectsEncryption+ "encryption-sym-aes128-s2k0.gpg"+ "symmetric-password.txt"+ "Simple S2K specifier"+ )+ , testCase+ "strict policy rejects non-encrypted packet after ESK prelude"+ testRejectsNonEncryptedAfterESKPrelude+ , testCase+ "strict policy rejects SEIPDv2 with only misaligned ESK versions"+ testRejectsMisalignedESKVersionForSEIPDv2+ , testCase+ "strict policy discards misaligned ESK when aligned ESK is present"+ testDiscardsMisalignedESKWhenAlignedCandidateExists+ , testCase+ "strict policy rejects trailing data after SEIPD v2"+ testTrailingDataRejectedStrictSEIPDv2+ , testCase+ "lenient policy reports DecryptTrailingData for trailing packet after SEIPD v2"+ testTrailingDataReportedLenientSEIPDv2+ , testCase+ "conduitDecryptChecked reports DecryptClean for well-formed SEIPD v2"+ testDecryptCleanSEIPDv2+ , testCase+ "conduitDecrypt matches conduitDecryptChecked (default)"+ testOptionsMatchesCheckedDefaultSEIPDv2+ , testCase+ "conduitDecrypt matches legacy conduitDecrypt output (default)"+ testOptionsMatchesLegacyDefaultOutputSEIPDv2+ , testCase+ "conduitDecrypt matches checked lenient trailing behavior"+ testOptionsMatchesCheckedLenientTrailingSEIPDv2+ ]+ )+ , testGroup+ "Encrypted secret keys"+ [ testCase+ "SUSSHA1 CAST5 IteratedSalted SHA1 RSA"+ (testSecretKeyDecryption "simple.seckey" "pki-password.txt")+ , testCase+ "SUS16bit CAST5 IteratedSalted SHA1 RSA"+ (testSecretKeyDecryption "16bitcksum.seckey" "pki-password.txt")+ , testCase+ "SUSSHA1 AES256 IteratedSalted SHA512 RSA"+ (testSecretKeyDecryption "aes256-sha512.seckey" "pki-password.txt")+ , testCase+ "SUSSHA1 AES128 IteratedSalted SHA256 ECDSA"+ ( testSecretKeyDecryption+ "nist_p-256_secretkey.gpg"+ "pki-password.txt"+ )+ ]+ , testGroup+ "Encrypting secret keys"+ [ testCase+ "legacy secret key encryption rejects implicit SHA-1 protection"+ ( testLegacySecretKeyEncryptionRejected+ "unencrypted.seckey"+ "pki-password.txt"+ )+ , testCase+ "SUSym secret key roundtrips"+ testSUSymSecretKeyRoundTrip+ , testCase+ "v6 secret key encryption roundtrips with SUSAEAD"+ testV6SecretKeyEncryptionRoundTrip+ , testCase+ "v4 AEAD OCB secret key compatibility roundtrip"+ testV4SecretKeyAEADOCBRoundTripCompat+ ]+ , testGroup+ "decrypt conduit stuff"+ [ testCase+ "conduitDecrypt supports PKESKv6 with raw session-key callback"+ testConduitDecryptSEIPDv2WithPKESKv6RawSessionKey+ , testCase+ "conduitDecrypt rejects invalid PKESKv6 raw session-key length"+ testConduitDecryptSEIPDv2RejectsWrongPKESKv6RawSessionKeyLength+ , testCase+ "conduitDecrypt unwraps PKESK RSA session keys in-library"+ testConduitDecryptSEIPDv2WithPKESKRSAUnwrap+ , testCase+ "conduitDecrypt probes wildcard callback fallback for PKESKv3 RSA key-id packets"+ testConduitDecryptSEIPDv2WithPKESKRSAUnwrapViaWildcardCallbackFallback+ , testCase+ "conduitDecrypt unwraps PKESKv3 RSA session keys via SHA1-CFB protected key loaded from file"+ testConduitDecryptSEIPDv2WithPKESKRSAUnwrapFromProtectedKey+ , testCase+ "parsed RSA secret key decrypts PKCS#1 v1.5 payload"+ testParsedRSASecretKeyPKCS15DecryptNotMessageNotRecognized+ , testCase+ "conduitDecrypt unwraps PKESK ECDH session keys in-library"+ testConduitDecryptSEIPDv2WithPKESKECDHUnwrap+ , testCase+ "conduitDecrypt rejects ECDH ephemeral points with wrong curve length"+ testConduitDecryptSEIPDv2RejectsECDHWrongEphemeralPointLength+ , testCase+ "conduitDecrypt unwraps PKESKv3 X25519 session keys in-library"+ testConduitDecryptSEIPDv2WithPKESKX25519V3Unwrap+ , testCase+ "conduitDecrypt retries wildcard PKESKv3 keys across callback order"+ testConduitDecryptSEIPDv2RetriesWildcardPKESKv3AcrossRecipientKeyOrder+ , testCase+ "conduitDecrypt retries wildcard PKESKv3 keys across long callback order"+ testConduitDecryptSEIPDv2RetriesWildcardPKESKv3AcrossLongRecipientKeyOrder+ , testCase+ "conduitDecrypt retries wildcard PKESKv3 keys across long callback order for SEIPDv1"+ testConduitDecryptSEIPDv1RetriesWildcardPKESKv3AcrossLongRecipientKeyOrder+ , testCase+ "unwrap callback decrypts wildcard PKESKv3 via candidates list"+ testMkCandidateResolverDecryptsWildcardPKESKv3+ , testCase+ "conduitDecrypt wildcard resolver receives typed previous-failure diagnostics"+ testConduitDecryptWildcardResolverProvidesTypedPreviousFailures+ , testCase+ "conduitDecryptWithReport surfaces wildcard resolver diagnostics"+ testConduitDecryptWithReportCapturesWildcardResolverDiagnostics+ , testCase+ "conduitDecrypt rejects PKESKv3 X25519 ephemeral values with wrong length"+ testConduitDecryptSEIPDv2RejectsPKESKX25519V3WrongEphemeralLength+ , testCase+ "conduitDecrypt falls back from Argon2 SKESK to PKESKv3 X25519"+ testConduitDecryptSEIPDv2FallsBackFromArgon2SKESKToPKESKv3X25519+ , testCase+ "conduitDecrypt retries earlier Argon2 SKESKs when latest is unusable"+ testConduitDecryptSEIPDv2FallsBackToEarlierArgon2SKESK+ , testCase+ "conduitDecrypt rejects ECDH KDF/KEK params outside RFC9580 Table 30"+ testConduitDecryptSEIPDv2RejectsECDHNonTable30Params+ , testCase+ "conduitDecrypt allows v4 Curve25519Legacy RFC6637 accepted parameters"+ testConduitDecryptSEIPDv2AllowsV4Curve25519LegacyRFC6637AcceptedParams+ , testCase+ "conduitDecrypt allows v4 Curve25519Legacy with truncated wrapped MPI"+ testConduitDecryptSEIPDv2AllowsV4Curve25519LegacyWithTruncatedWrappedMPI+ , testCase+ "conduitDecrypt keeps v6 Curve25519Legacy ECDH strict"+ testConduitDecryptSEIPDv2RejectsV6Curve25519LegacyNonTable30Params+ , testCase+ "conduitDecrypt unwraps PKESK X448 session keys in-library"+ testConduitDecryptSEIPDv2WithPKESKX448Unwrap+ , testCase+ "conduitDecrypt rejects PKESKv6 X448 ephemeral values with wrong length"+ testConduitDecryptSEIPDv2RejectsPKESKX448WrongEphemeralLength+ , testCase+ "encrypt-side session material encoding uses OpenPGP format"+ testEncodeOpenPGPSessionMaterial+ , testCase+ "encrypt-side PKESK builder wraps RSA recipient session key"+ testBuildPKESKPayloadForRecipientRSA+ , testCase+ "encrypt-side PKESKv3 builder supports RSA v4 interop"+ testBuildPKESKv3PayloadForRecipientRSAInterop+ , testCase+ "encrypt-side PKESKv3 builder supports v6 recipients"+ testBuildPKESKv3PayloadForRecipientSupportsV6Key+ , testCase+ "encrypt-side PKESKv3 builder supports RFC6637 ECDH recipients"+ testBuildPKESKv3PayloadForRecipientECDHInterop+ , testCase+ "PKESKv3 ECDH serializer uses RFC 6637 wire format"+ testPKESKv3ECDHWireFormat+ , testCase+ "encrypt-side PKESKv3 builder supports RFC6637 Curve25519Legacy recipients"+ testBuildPKESKv3PayloadForRecipientCurve25519LegacyInterop+ , testCase+ "encrypt-side PKESKv3 builder rejects non-RFC6637 Curve448Legacy recipients"+ testBuildPKESKv3PayloadForRecipientRejectsCurve448Legacy+ , testCase+ "RFC 9580 v6 X25519 PKESK uses raw-key semantics, not v3-encoded material"+ testBuildPKESKv6PayloadForRecipientX25519RawKeySemantics+ , testCase+ "RFC 9580 v6 X448 PKESK uses raw-key semantics, not v3-encoded material"+ testBuildPKESKv6PayloadForRecipientX448RawKeySemantics+ , testCase+ "encrypt-side policy builder emits PKESK3 for ForceV3Interop"+ testBuildPKESKPayloadForRecipientWithPolicyForceV3Interop+ , testCase+ "encrypt-side profile helper honors recipient capability hints"+ testRecipientVersionStrategyForProfileHonorsHints+ , testCase+ "encryptForRecipients auto-detects mixed recipient PKESK strategies"+ testEncryptRecipientsWithRequestAutoDetectsMixedRecipientStrategies+ , testCase+ "encryptForRecipients negotiates symmetric algorithm from recipient capabilities when enabled"+ testEncryptRecipientsNegotiatesSymmetricAlgorithmWhenEnabled+ , testCase+ "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+ "recipientCapabilitiesFromSubpacketPayloads extracts preferred AEAD algorithms"+ testRecipientCapabilitiesFromSubpacketPayloadsExtractsPreferredAEADAlgorithms+ , testCase+ "encryptForRecipients negotiates AEAD algorithm from recipient capabilities when enabled"+ testEncryptRecipientsNegotiatesAEADAlgorithmWhenEnabled+ , testCase+ "encryptForRecipients capability negotiation fails when recipients share no AEAD algorithm"+ testEncryptRecipientsNegotiationFailsWithoutCommonAEADAlgorithm+ , testCase+ "encryptForRecipients falls back to SEIPDv1 when recipients do not advertise SEIPDv2 support"+ testEncryptRecipientsSEIPDv2FallsBackWhenRecipientsDoNotAdvertiseV2+ , testCase+ "encryptForRecipients rejects SEIPDv1 when recipients do not advertise MDC support"+ testEncryptRecipientsRejectsSEIPDv1WhenRecipientsDoNotAdvertiseMDC+ , testCase+ "recipientEncryptionTargetsFromTK returns only encryption-capable candidates"+ testRecipientEncryptionTargetsFromTKIncludesOnlyEncryptionCapableKeys+ , testCase+ "recipientEncryptionTargetFromTK prefers encryption-capable subkeys"+ testRecipientEncryptionTargetFromTKPrefersSubkeyOverPrimary+ , testCase+ "recipientEncryptionTargetFromTK rejects TKs without encryptable key material"+ testRecipientEncryptionTargetFromTKRejectsTKWithoutEncryptableKeys+ , testCase+ "recipientEncryptionTargetsReportFromTKAtTimestamp reports sign-only subkey rejection reasons"+ testRecipientEncryptionTargetsReportFromTKAtTimestampExplainsRejections+ , testCase+ "recipientEncryptionTargetsReportFromTKAtTimestamp reports unsupported algorithm rejections"+ testRecipientEncryptionTargetsReportFromTKAtTimestampExplainsUnsupportedAlgorithms+ , testCase+ "recipientEncryptionTargetsReportFromTKAtTimestamp rejects expired subkeys"+ testRecipientEncryptionTargetsReportFromTKAtTimestampRejectsExpiredSubkey+ , testCase+ "recipientEncryptionTargetsReportFromTKAtTimestamp rejects revoked subkeys"+ testRecipientEncryptionTargetsReportFromTKAtTimestampRejectsRevokedSubkey+ , testCase+ "recipientEncryptionTargetsFromTKAtTimestamp extracts effective self-signature capabilities"+ testRecipientEncryptionTargetsFromTKAtTimestampExtractsSelfSigCapabilities+ , testCase+ "recipientEncryptionTargetFromTKAtTimestamp filters subkey self-signatures by timestamp"+ testRecipientEncryptionTargetFromTKAtTimestampAppliesTimestampFiltering+ , testCase+ "recipientEncryptionTargetFromTKAtTimestampWithPolicy can prefer primary key"+ testRecipientEncryptionTargetFromTKAtTimestampWithPolicyPrefersPrimary+ , testCase+ "recipientEncryptionTargetFromTKAtTimestampWithPolicy can prefer newest key"+ testRecipientEncryptionTargetFromTKAtTimestampWithPolicyPrefersNewest+ , testCase+ "recipientEncryptionTargetsFromTK excludes subkeys with non-encrypt key flags"+ testRecipientEncryptionTargetsFromTKRejectsKeyWithNonEncryptFlags+ , testCase+ "recipientEncryptionTargetsFromTK includes subkeys with no key flags advertised"+ testRecipientEncryptionTargetsFromTKIncludesKeyWithNoFlags+ , testCase+ "encryptForRecipients optionally emits one-pass signature packets"+ testEncryptRecipientsWithRequestEmitsOnePassSignatures+ , testCase+ "encryptForRecipients one-pass mode requires issuer metadata"+ testEncryptRecipientsWithRequestRejectsOnePassWhenIssuerMetadataMissing+ , testCase+ "encryptForRecipients with EncryptInteropLegacy produces SEIPDv1 and PKESKv3"+ testEncryptInteropLegacyProducesSEIPDv1+ , testCase+ "encrypt-side PKESK builder reports unsupported recipient algorithms"+ testBuildPKESKPayloadUnsupportedRecipient+ , testCase+ "encrypt-side ECDH PKESK builder rejects SHA1 KDF policy"+ testBuildPKESKPayloadRejectsECDHSHA1+ , testCase+ "encrypt-side canonicalize PKESK recipient id helper"+ testCanonicalizePKESKRecipientIdHelper+ , testCase+ "conduitDecrypt decrypts seipdv2 fixture with matching v6 secret key"+ testConduitDecryptSEIPDv2FixtureWithMatchingV6SecretKey+ , testCase+ "seipdv2-for-v4-key fixture parses as PKESKv6+SEIPDv2"+ testSEIPDv2ForV4KeyArmor+ , testCase+ "conduitDecrypt decrypts seipdv2-for-v4-key fixture with matching v4 secret key"+ testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKey+ , testCase+ "conduitDecrypt tries earlier PKESKs when latest is unusable"+ testConduitDecryptSEIPDv2FixtureIgnoresUnusableLatestPKESK+ , testCase+ "conduitDecrypt matches valid recipient-id forms without caller retries"+ testConduitDecryptSEIPDv2FixtureAcceptsRecipientIdWithoutCallerPermutations+ , testCase+ "conduitDecrypt fails PKESK exhaustion without manual session-key prompt"+ testConduitDecryptPKESKFailureDoesNotRequireManualSessionMaterial+ , testCase+ "seipdv2-two-recipients fixture parses as PKESKv6*2+SEIPDv2"+ testSEIPDv2TwoRecipientsArmor+ , testCase+ "seipdv2-three-recipients fixture parses as PKESKv6*3+SEIPDv2"+ testSEIPDv2ThreeRecipientsArmor+ , testCase+ "conduitDecrypt decrypts seipdv2-two-recipients fixture with matching v4 secret key"+ testConduitDecryptSEIPDv2TwoRecipientsFixtureWithMatchingV4SecretKey+ , testCase+ "conduitDecrypt decrypts reordered+bogus seipdv2-two-recipients PKESKs"+ testConduitDecryptSEIPDv2TwoRecipientsFixtureWithReorderedAndUnusablePKESKs+ , testCase+ "conduitDecrypt decrypts seipdv2-three-recipients fixture with matching v4 secret key"+ testConduitDecryptSEIPDv2ThreeRecipientsFixtureWithMatchingV4SecretKey+ , testCase+ "conduitDecrypt decrypts reordered+bogus seipdv2-three-recipients PKESKs"+ testConduitDecryptSEIPDv2ThreeRecipientsFixtureWithReorderedAndUnusablePKESKs+ ]+ , testGroup+ "ESK/SKESK/ArgonS2K"+ [ testCase+ "encoded session key rejects too-short payloads"+ testDecodeOpenPGPEncodedSessionKeyRejectsTooShort+ , testCase+ "encoded session key rejects algorithm/key-length mismatches"+ testDecodeOpenPGPEncodedSessionKeyRejectsLengthMismatch+ , testCase+ "encoded session key rejects checksum mismatches"+ testDecodeOpenPGPEncodedSessionKeyRejectsChecksumMismatch+ , testCase+ "unknown SKESK version is rejected"+ testSKESKRejectsUnknownVersion+ , testCase+ "v4 SKESK encrypted session keys reject Simple S2K"+ testSKESK4EncryptedSessionKeyRejectsSimpleS2K+ , testCase+ "v4 SKESK Argon2 encrypted ESK roundtrip across AES-128/192/256"+ testSKESK4Argon2EncryptedSessionKeyRoundTripAcrossAES+ , testCase+ "v4 SKESK Argon2 encrypted ESK rejects legacy checksum trailer"+ testSKESK4Argon2EncryptedSessionKeyRejectsTrailingChecksum+ , testCase+ "passphrase SKESK policy ForceV4Interop emits SKESKv4 + SEIPDv1"+ testEncryptPassphraseWithPolicyForceV4Interop+ , testCase+ "passphrase SKESK policy PreferV6 emits SKESKv6 + SEIPDv2"+ testEncryptPassphraseWithPolicyPreferV6+ , testCase "Argon2 S2K derivation vector" testArgon2S2KVector+ , testCase "Argon2 S2K Ord instance is total" testArgon2S2KOrdTotal+ ]+ ]++testCompression :: FilePath -> Assertion+testCompression fpr = do+ bs <- BL.readFile $ "tests/data/" ++ fpr+ let firstpass =+ fmap (concatMap (either (const []) id . decompressPkt) . unBlock)+ . runGet get+ $ bs+ case firstpass of+ Left _ -> assertFailure $ "First pass failed on " ++ fpr+ Right [] ->+ assertFailure $ "First pass of " ++ fpr ++ " decoded to nothing."+ Right packs -> do+ let roundtrip = runPut $ put . Block $ [compressPkts ZIP packs]+ let secondpass =+ fmap (concatMap (either (const []) id . decompressPkt) . unBlock)+ . runGet get+ $ roundtrip+ if secondpass == Right []+ then+ assertFailure $+ "Second pass of " ++ fpr ++ " decoded to nothing."+ else assertEqual ("for " ++ fpr) firstpass secondpass++counter :: (Monad m) => DC.ConduitT a DC.Void m Int+counter = CL.fold (const . (1 +)) 0++testConduitOutputLength+ :: FilePath+ -> DC.ConduitT B.ByteString b (ResourceT IO) ()+ -> Int+ -> Assertion+testConduitOutputLength fpr c target = do+ len <-+ DC.runConduitRes $+ CB.sourceFile ("tests/data/" ++ fpr) DC..| c DC..| counter+ assertEqual ("expected length " ++ show target) target len++testConduitToTKsEitherReportsParseFailure :: Assertion+testConduitToTKsEitherReportsParseFailure = do+ results <-+ DC.runConduitRes $+ CB.sourceFile "tests/data/uncompressed-ops-rsa.gpg"+ DC..| conduitGet get+ DC..| conduitToTKsEither+ DC..| CL.consume+ assertBool+ "conduitToTKsEither should report parse failures for non-key packet streams"+ (any (either (const True) (const False)) results)++testConduitToTKsDroppingEitherReportsParseFailure :: Assertion+testConduitToTKsDroppingEitherReportsParseFailure = do+ results <-+ DC.runConduitRes $+ CB.sourceFile "tests/data/uncompressed-ops-rsa.gpg"+ DC..| conduitGet get+ DC..| conduitToTKsDroppingEither+ DC..| CL.consume+ assertBool+ "conduitToTKsDroppingEither should report parse failures for non-key packet streams"+ (any (either (const True) (const False)) results)++testConduitToSomeTKsEitherReportsParseFailure :: Assertion+testConduitToSomeTKsEitherReportsParseFailure = do+ results <-+ DC.runConduitRes $+ CB.sourceFile "tests/data/uncompressed-ops-rsa.gpg"+ DC..| conduitGet get+ DC..| conduitToSomeTKsEither+ DC..| CL.consume+ assertBool+ "conduitToSomeTKsEither should report parse failures for non-key packet streams"+ (any (either (const True) (const False)) results)++testConduitToSomeTKsDroppingEitherReportsParseFailure+ :: Assertion+testConduitToSomeTKsDroppingEitherReportsParseFailure = do+ results <-+ DC.runConduitRes $+ CB.sourceFile "tests/data/uncompressed-ops-rsa.gpg"+ DC..| conduitGet get+ DC..| conduitToSomeTKsDroppingEither+ DC..| CL.consume+ assertBool+ "conduitToSomeTKsDroppingEither should report parse failures for non-key packet streams"+ (any (either (const True) (const False)) results)++-- This needs a lot of work++{- | Decrypt a symmetric-encrypted fixture using 'defaultDecryptPolicy' and+assert the packet structure is SKESK + SEIPDv1 and the cleartext matches.+Only suitable for AES-128/192/256 fixtures with iterated-salted S2K.+-}+testSymmetricEncryption+ :: FilePath -> FilePath -> BL.ByteString -> Assertion+testSymmetricEncryption encfile passfile cleartext = do+ passphrase <- readFixtureLazy passfile+ pt <- readFixturePackets encfile+ assertEqual "wrong number of packets" 2 (length pt)+ skesk <-+ case pt of+ [] ->+ assertFailure+ ( "expected SKESK packet in "+ ++ encfile+ ++ " but packet list was empty"+ )+ >> fail "empty fixture packet list"+ (firstPkt : _) ->+ case (fromPktEither firstPkt :: Either String (SKESK 'SKESKV4)) of+ Left err ->+ assertFailure ("failed to coerce first packet to SKESK: " ++ err)+ >> fail err+ Right x -> pure x+ assertEqual+ "first packet should be SKESK"+ SKESKType+ (packetType skesk)+ case last pt of+ SymEncIntegrityProtectedDataPkt (SEIPD1 1 _) -> pure ()+ other ->+ assertFailure+ ( "second packet should be SEIPDv1 (tag 18, version 1), got: "+ ++ show other+ )+ decrypted <-+ catch+ ( DC.runConduitRes $+ CL.sourceList pt+ DC..| conduitDecrypt (fakeCallback passphrase)+ DC..| CL.consume+ )+ ( \e -> do+ let err = show (e :: SomeException)+ assertFailure ("decryption threw exception: " ++ err)+ )+ payload <-+ case decrypted of+ [] ->+ assertFailure ("decryption produced no packets for " ++ encfile)+ >> fail "expected decrypted literal packet"+ (firstDecrypted : _) ->+ case (fromPktEither firstDecrypted :: Either String LiteralData) of+ Left err ->+ assertFailure+ ("failed to coerce decrypted packet to literal data: " ++ err)+ >> fail err+ Right x -> pure (_literalDataPayload x)+ assertEqual ("cleartext for " ++ encfile) cleartext payload+ where+ fakeCallback :: BL.ByteString -> String -> IO BL.ByteString+ fakeCallback = const . return++testSEIPDv1ResyncNonceMdcRoundTrip :: Assertion+testSEIPDv1ResyncNonceMdcRoundTrip = do+ let sa = AES256+ keyBytes = B.pack [0 .. 31]+ iv = IV (B.pack [32 .. 47])+ cleartext = B.pack [1 .. 80]+ cleartextWithMDC = cleartext <> mdcTrailerForSEIPDv1 iv cleartext+ ciphertext <-+ either+ ( \e ->+ assertFailure ("encryptOpenPGPCfbRaw failed: " ++ show e)+ >> pure mempty+ )+ pure+ ( encryptOpenPGPCfbRaw+ OpenPGPCFBResyncW+ sa+ iv+ cleartextWithMDC+ keyBytes+ )+ (nonce, decrypted) <-+ either+ ( \e ->+ assertFailure ("decryptOpenPGPCfbWithNonce failed: " ++ show e)+ >> pure (mempty, mempty)+ )+ pure+ (decryptOpenPGPCfbWithNonce sa ciphertext keyBytes)+ assertEqual+ "SEIPDv1 nonce should match IV||IV[-2:]"+ (seipdv1NonceFromIV iv)+ nonce+ assertEqual+ "decrypted bytes should include payload+MDC"+ cleartextWithMDC+ decrypted+ payloadOut <-+ either+ ( \err ->+ assertFailure ("validateSEIPD1MDC failed: " ++ err)+ >> pure mempty+ )+ pure+ (validateSEIPD1MDC nonce decrypted)+ assertEqual+ "MDC-verified payload should round-trip"+ cleartext+ payloadOut++{- | Like 'testSymmetricEncryption' but uses 'lenientDecryptPolicy' to permit+legacy RFC4880/RFC2440 message formats (SED, deprecated S2K, old ciphers).+The 'Bool' indicates whether the fixture uses SEIPDv1 (True) or SED (False).+-}+testLegacySymmetricEncryption+ :: Bool -> FilePath -> FilePath -> BL.ByteString -> Assertion+testLegacySymmetricEncryption expectSEIPDv1 encfile passfile cleartext = do+ passphrase <- readFixtureLazy passfile+ pt <- readFixturePackets encfile+ assertEqual "wrong number of packets" 2 (length pt)+ skesk <-+ case pt of+ [] ->+ assertFailure+ ( "expected SKESK packet in "+ ++ encfile+ ++ " but packet list was empty"+ )+ >> fail "empty fixture packet list"+ (firstPkt : _) ->+ case (fromPktEither firstPkt :: Either String (SKESK 'SKESKV4)) of+ Left err ->+ assertFailure ("failed to coerce first packet to SKESK: " ++ err)+ >> fail err+ Right x -> pure x+ assertEqual+ "first packet should be SKESK"+ SKESKType+ (packetType skesk)+ case last pt of+ SymEncDataPkt {}+ | not expectSEIPDv1 -> pure ()+ | otherwise ->+ assertFailure+ "expected SEIPDv1 packet (tag 18) but got SED (tag 9)"+ SymEncIntegrityProtectedDataPkt (SEIPD1 1 _)+ | expectSEIPDv1 -> pure ()+ | otherwise ->+ assertFailure+ "expected SED packet (tag 9) but got SEIPDv1 (tag 18)"+ other -> assertFailure ("unexpected second packet: " ++ show other)+ decrypted <-+ catch+ ( DC.runConduitRes $+ CL.sourceList pt+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ (\_ -> pure Nothing)+ (fakeCallback passphrase)+ DC..| CL.consume+ )+ ( \e -> do+ let err = show (e :: SomeException)+ assertFailure ("decryption threw exception: " ++ err)+ )+ payload <-+ case decrypted of+ [] ->+ assertFailure ("decryption produced no packets for " ++ encfile)+ >> fail "expected decrypted literal packet"+ (firstDecrypted : _) ->+ case (fromPktEither firstDecrypted :: Either String LiteralData) of+ Left err ->+ assertFailure+ ("failed to coerce decrypted packet to literal data: " ++ err)+ >> fail err+ Right x -> pure (_literalDataPayload x)+ assertEqual ("cleartext for " ++ encfile) cleartext payload+ where+ fakeCallback :: BL.ByteString -> String -> IO BL.ByteString+ fakeCallback = const . return++{- | Assert that decrypting with the strict 'defaultDecryptPolicy' throws an+exception whose message contains the given substring.+-}+testStrictPolicyRejectsEncryption+ :: FilePath -> FilePath -> String -> Assertion+testStrictPolicyRejectsEncryption encfile passfile expectedFragment = do+ passphrase <- readFixtureLazy passfile+ pt <- readFixturePackets encfile+ result <-+ try+ ( DC.runConduitRes $+ CL.sourceList pt+ DC..| conduitDecrypt (fakeCallback passphrase)+ DC..| CL.consume+ )+ :: IO (Either SomeException [Pkt])+ case result of+ Right _ ->+ assertFailure $+ "Expected strict policy to reject '"+ ++ encfile+ ++ "' with error containing '"+ ++ expectedFragment+ ++ "', but decryption succeeded"+ Left err ->+ assertBool+ ( "Expected error containing '"+ ++ expectedFragment+ ++ "', got: "+ ++ show err+ )+ (expectedFragment `isInfixOf` show err)+ where+ fakeCallback :: BL.ByteString -> String -> IO BL.ByteString+ fakeCallback = const . return++-- | Decrypt a file and check the 'DecryptOutcome'.+testDecryptOutcome+ :: FilePath -> FilePath -> DecryptOutcome -> Assertion+testDecryptOutcome encfile passfile expectedOutcome = do+ passphrase <- readFixtureLazy passfile+ pt <- readFixturePackets encfile+ (outcome, _pkts) <-+ catch+ ( DC.runConduitRes $+ CL.sourceList pt+ DC..| fuseBoth+ (conduitDecryptChecked (fakeCallback passphrase))+ CL.consume+ )+ ( \e -> do+ let err = show (e :: SomeException)+ assertFailure ("decryption threw unexpected exception: " ++ err)+ fail "unreachable"+ )+ assertEqual+ ("DecryptOutcome for " ++ encfile)+ expectedOutcome+ outcome+ where+ fakeCallback :: BL.ByteString -> String -> IO BL.ByteString+ fakeCallback = const . return++{- | Build a synthetic packet list with an appended trailing packet, decrypt+with lenient policy, and verify 'DecryptTrailingData' is reported.+-}+testTrailingDataReportedLenient+ :: FilePath -> FilePath -> Assertion+testTrailingDataReportedLenient encfile passfile = do+ passphrase <- readFixtureLazy passfile+ pt <- readFixturePackets encfile+ let trailingPkt = OtherPacketPkt 0xFE ""+ ptWithTrailing = pt ++ [trailingPkt]+ (outcome, _pkts) <-+ catch+ ( DC.runConduitRes $+ CL.sourceList ptWithTrailing+ DC..| fuseBoth+ ( conduitDecryptCheckedWithDecryptPolicy+ lenientDecryptPolicy+ (\_ -> pure Nothing)+ (fakeCallback passphrase)+ )+ CL.consume+ )+ ( \e -> do+ let err = show (e :: SomeException)+ assertFailure+ ("lenient decrypt threw unexpected exception: " ++ err)+ fail "unreachable"+ )+ assertEqual+ ("DecryptTrailingData for " ++ encfile)+ DecryptTrailingData+ outcome+ where+ fakeCallback :: BL.ByteString -> String -> IO BL.ByteString+ fakeCallback = const . return++{- | Same as above but with strict policy: should throw "after message+integrity boundary".+-}+testTrailingDataRejectedStrict+ :: FilePath -> FilePath -> Assertion+testTrailingDataRejectedStrict encfile passfile = do+ passphrase <- readFixtureLazy passfile+ pt <- readFixturePackets encfile+ let trailingPkt = OtherPacketPkt 0xFE ""+ ptWithTrailing = pt ++ [trailingPkt]+ result <-+ try+ ( DC.runConduitRes $+ CL.sourceList ptWithTrailing+ DC..| fuseBoth+ (conduitDecryptChecked (fakeCallback passphrase))+ CL.consume+ )+ :: IO (Either SomeException (DecryptOutcome, [Pkt]))+ case result of+ Left err ->+ assertFailure+ ( "Expected DecryptMalformedStructure but got exception: "+ ++ show err+ )+ Right (DecryptMalformedStructure malformedReason, _) ->+ assertBool+ ( "Expected 'after message integrity boundary', got: "+ ++ malformedReason+ )+ ("after message integrity boundary" `isInfixOf` malformedReason)+ Right (other, _) ->+ assertFailure+ ("Expected DecryptMalformedStructure but got: " ++ show other)+ where+ fakeCallback :: BL.ByteString -> String -> IO BL.ByteString+ fakeCallback = const . return++encryptedSEIPDv2Packets :: IO [Pkt]+encryptedSEIPDv2Packets = do+ let passphrase =+ mkPassphrase+ (BL.pack (map (fromIntegral . fromEnum) ("test" :: String)))+ payload =+ mkClearPayload+ (BL.pack (map (fromIntegral . fromEnum) ("hello" :: String)))+ let result =+ encryptMessageDefault+ DoNotExposeSessionMaterial+ passphrase+ payload+ case result of+ Left err ->+ fail+ ( "encryptedSEIPDv2Packets: encryptMessageDefault failed: "+ ++ show err+ )+ Right (encPayload, _) ->+ DC.runConduitRes $+ CB.sourceLbs (encryptedPayloadBytes encPayload)+ DC..| conduitGet get+ DC..| CL.consume++defaultOptionsForPassphrase :: BL.ByteString -> DecryptOptions+defaultOptionsForPassphrase passphrase =+ DecryptOptions+ { decryptOptionsKeyResolution = DecryptWithoutPKESK+ , decryptOptionsPolicy = defaultDecryptPolicy+ , decryptOptionsPassphraseCallback = const (pure passphrase)+ }++testDecryptCleanSEIPDv2 :: Assertion+testDecryptCleanSEIPDv2 = do+ pt <- encryptedSEIPDv2Packets+ let passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))+ cb = const (pure passphrase)+ (outcome, _) <-+ catch+ ( DC.runConduitRes $+ CL.sourceList pt+ DC..| fuseBoth (conduitDecryptChecked cb) CL.consume+ )+ ( \e ->+ assertFailure+ ("unexpected exception: " ++ show (e :: SomeException))+ >> fail "unreachable"+ )+ assertEqual+ "clean SEIPD v2 should yield DecryptClean"+ DecryptClean+ outcome++testOptionsMatchesCheckedDefaultSEIPDv2 :: Assertion+testOptionsMatchesCheckedDefaultSEIPDv2 = do+ pt <- encryptedSEIPDv2Packets+ let passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))+ cb = const (pure passphrase)+ opts = defaultOptionsForPassphrase passphrase+ checkedResult <-+ DC.runConduitRes $+ CL.sourceList pt+ DC..| fuseBoth (conduitDecryptChecked cb) CL.consume+ optionsResult <-+ DC.runConduitRes $+ CL.sourceList pt+ DC..| fuseBoth (DCD.conduitDecrypt opts) CL.consume+ assertEqual+ "options conduit should match checked conduit on clean decrypt"+ checkedResult+ optionsResult++testOptionsMatchesLegacyDefaultOutputSEIPDv2 :: Assertion+testOptionsMatchesLegacyDefaultOutputSEIPDv2 = do+ pt <- encryptedSEIPDv2Packets+ let passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))+ cb = const (pure passphrase)+ opts = defaultOptionsForPassphrase passphrase+ legacyOutput <-+ DC.runConduitRes $+ CL.sourceList pt DC..| conduitDecrypt cb DC..| CL.consume+ (optionsOutcome, optionsOutput) <-+ DC.runConduitRes $+ CL.sourceList pt+ DC..| fuseBoth (DCD.conduitDecrypt opts) CL.consume+ assertEqual+ "options conduit should report DecryptClean for clean stream"+ DecryptClean+ optionsOutcome+ assertEqual+ "legacy conduit output should match options conduit output"+ legacyOutput+ optionsOutput++testTrailingDataRejectedStrictSEIPDv2 :: Assertion+testTrailingDataRejectedStrictSEIPDv2 = do+ pt <- encryptedSEIPDv2Packets+ let ptWithTrailing = pt ++ [OtherPacketPkt 0xFE ""]+ passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))+ cb = const (pure passphrase)+ result <-+ try+ ( DC.runConduitRes $+ CL.sourceList ptWithTrailing+ DC..| fuseBoth (conduitDecryptChecked cb) CL.consume+ )+ :: IO (Either SomeException (DecryptOutcome, [Pkt]))+ case result of+ Left err ->+ assertFailure+ ( "Expected DecryptMalformedStructure but got exception: "+ ++ show err+ )+ Right (DecryptMalformedStructure reason, _) ->+ assertBool+ ("Expected 'after message integrity boundary', got: " ++ reason)+ ("after message integrity boundary" `isInfixOf` reason)+ Right (other, _) ->+ assertFailure+ ( "Expected strict policy to reject trailing data but got: "+ ++ show other+ )++testTrailingDataReportedLenientSEIPDv2 :: Assertion+testTrailingDataReportedLenientSEIPDv2 = do+ pt <- encryptedSEIPDv2Packets+ let ptWithTrailing = pt ++ [OtherPacketPkt 0xFE ""]+ passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))+ cb = const (pure passphrase)+ (outcome, _) <-+ catch+ ( DC.runConduitRes $+ CL.sourceList ptWithTrailing+ DC..| fuseBoth+ ( conduitDecryptCheckedWithDecryptPolicy+ lenientDecryptPolicy+ (\_ -> pure Nothing)+ cb+ )+ CL.consume+ )+ ( \e ->+ assertFailure+ ("unexpected exception: " ++ show (e :: SomeException))+ >> fail "unreachable"+ )+ assertEqual+ "trailing packet should yield DecryptTrailingData"+ DecryptTrailingData+ outcome++testOptionsMatchesCheckedLenientTrailingSEIPDv2 :: Assertion+testOptionsMatchesCheckedLenientTrailingSEIPDv2 = do+ pt <- encryptedSEIPDv2Packets+ let ptWithTrailing = pt ++ [OtherPacketPkt 0xFE ""]+ passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))+ cb = const (pure passphrase)+ opts =+ DecryptOptions+ { decryptOptionsKeyResolution = DecryptWithoutPKESK+ , decryptOptionsPolicy = lenientDecryptPolicy+ , decryptOptionsPassphraseCallback = cb+ }+ checkedResult <-+ DC.runConduitRes $+ CL.sourceList ptWithTrailing+ DC..| fuseBoth+ ( conduitDecryptCheckedWithDecryptPolicy+ lenientDecryptPolicy+ (\_ -> pure Nothing)+ cb+ )+ CL.consume+ optionsResult <-+ DC.runConduitRes $+ CL.sourceList ptWithTrailing+ DC..| fuseBoth (DCD.conduitDecrypt opts) CL.consume+ assertEqual+ "options conduit should match checked lenient conduit with trailing data"+ checkedResult+ optionsResult++testRejectsNonEncryptedAfterESKPrelude :: Assertion+testRejectsNonEncryptedAfterESKPrelude = do+ pt <- encryptedSEIPDv2Packets+ let malformed =+ case pt of+ [] -> []+ esk : _ ->+ [ esk+ , LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ "not-encrypted"+ ]+ passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))+ cb = const (pure passphrase)+ result <-+ try+ ( DC.runConduitRes $+ CL.sourceList malformed+ DC..| fuseBoth (conduitDecryptChecked cb) CL.consume+ )+ :: IO (Either SomeException (DecryptOutcome, [Pkt]))+ case result of+ Left err ->+ assertFailure+ ( "Expected DecryptMalformedStructure but got exception: "+ ++ show err+ )+ Right (DecryptMalformedStructure reason, _) ->+ assertBool+ ("Expected ESK prelude shape failure, got: " ++ reason)+ ( "ESK packets must immediately precede encrypted data"+ `isInfixOf` reason+ )+ Right (other, _) ->+ assertFailure+ ("Expected DecryptMalformedStructure but got: " ++ show other)++testRejectsMisalignedESKVersionForSEIPDv2 :: Assertion+testRejectsMisalignedESKVersionForSEIPDv2 = do+ pt <- encryptedSEIPDv2Packets+ let malformed =+ case pt of+ ( SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 sa _ s2k _ _ _))+ : rest+ ) ->+ SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k Nothing))+ : rest+ _ -> pt+ passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))+ cb = const (pure passphrase)+ result <-+ try+ ( DC.runConduitRes $+ CL.sourceList malformed+ DC..| fuseBoth (conduitDecryptChecked cb) CL.consume+ )+ :: IO (Either SomeException (DecryptOutcome, [Pkt]))+ case result of+ Left err ->+ assertFailure+ ( "Expected DecryptMalformedStructure but got exception: "+ ++ show err+ )+ Right (DecryptMalformedStructure reason, _) ->+ assertBool+ ("Expected payload/ESK alignment failure, got: " ++ reason)+ ("version-aligned" `isInfixOf` reason)+ Right (other, _) ->+ assertFailure+ ("Expected DecryptMalformedStructure but got: " ++ show other)++testDiscardsMisalignedESKWhenAlignedCandidateExists :: Assertion+testDiscardsMisalignedESKWhenAlignedCandidateExists = do+ pt <- encryptedSEIPDv2Packets+ let withMixedPrelude =+ case pt of+ ( SKESKPkt+ (SKESKPayloadV6Packet (SKESKPayloadV6 sa aa s2k iv esk tag))+ : rest+ ) ->+ SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 sa s2k Nothing))+ : SKESKPkt+ (SKESKPayloadV6Packet (SKESKPayloadV6 sa aa s2k iv esk tag))+ : rest+ _ -> pt+ passphrase = BL.pack (map (fromIntegral . fromEnum) ("test" :: String))+ cb = const (pure passphrase)+ (outcome, _) <-+ catch+ ( DC.runConduitRes $+ CL.sourceList withMixedPrelude+ DC..| fuseBoth (conduitDecryptChecked cb) CL.consume+ )+ ( \e ->+ assertFailure+ ("unexpected exception: " ++ show (e :: SomeException))+ >> fail "unreachable"+ )+ assertEqual+ "misaligned ESKs should be discarded when an aligned ESK is present"+ DecryptClean+ outcome++testSecretKeyDecryption :: FilePath -> FilePath -> Assertion+testSecretKeyDecryption keyfile passfile = do+ passphrase <- readFixtureLazy passfile+ kr <-+ DC.runConduitRes $+ CB.sourceFile (fixturePath keyfile)+ DC..| conduitGet get+ DC..| CL.consume+ SecretKey pkp ska <-+ case kr of+ [] ->+ assertFailure+ ("no packets found in secret key fixture " ++ keyfile)+ >> fail "empty secret key fixture"+ (firstPkt : _) ->+ case (fromPktEither firstPkt :: Either String SecretKey) of+ Left err ->+ assertFailure+ ("failed to coerce key packet to SecretKey: " ++ err)+ >> fail err+ Right x -> pure x+ decrypted <-+ case decryptPrivateKey (pkp, ska) passphrase of+ Left err ->+ assertFailure ("secret key decryption failed: " ++ err)+ >> fail "secret key decryption failed"+ Right x -> pure x+ case decrypted of+ SUUnencrypted skey _ -> doPkeyAndSkeyMatch (_pubkey pkp) skey+ other ->+ assertFailure+ ( "secret key decryption should produce an unencrypted secret key, got: "+ ++ show other+ )++testLegacySecretKeyEncryptionRejected+ :: FilePath -> FilePath -> Assertion+testLegacySecretKeyEncryptionRejected keyfile passfile = do+ passphrase <- readFixtureLazy passfile+ kr <-+ DC.runConduitRes $+ CB.sourceFile (fixturePath keyfile)+ DC..| conduitGet get+ DC..| CL.consume+ SecretKey pkp ska <-+ case kr of+ [] ->+ assertFailure+ ("no packets found in secret key fixture " ++ keyfile)+ >> fail "empty secret key fixture"+ (firstPkt : _) ->+ case (fromPktEither firstPkt :: Either String SecretKey) of+ Left err ->+ assertFailure+ ("failed to coerce key packet to SecretKey: " ++ err)+ >> fail err+ Right x -> pure x+ case encryptPrivateKeyWithPolicyAndSaltAndIV+ defaultPolicy+ pkp+ (Salt "\226~\197\a\202#\"G")+ (IV "\187\219\253I\236\204\t5D\196\NAK>;\202\185\t")+ ska+ passphrase of+ Left err | "explicit legacy override required" `isInfixOf` err -> pure ()+ Left err ->+ assertFailure+ ( "legacy secret key encryption should be rejected with a policy error, got: "+ ++ err+ )+ Right _ ->+ assertFailure+ "legacy secret key encryption should reject implicit SHA-1 protection"++testV6SecretKeyEncryptionRoundTrip :: Assertion+testV6SecretKeyEncryptionRoundTrip = do+ passphrase <- readPKIPassphrase+ armors <- loadArmor "v6-secret.pgp.aa"+ armor <-+ case armors of+ (a : _) -> pure a+ [] ->+ assertFailure+ "v6-secret.pgp.aa should contain one armored payload"+ >> fail "expected one armored payload"+ let packets = parsePkts (armorPayload armor)+ SecretKey pkp ska <-+ case [ sk+ | pkt <- packets+ , Right sk <- [fromPktEither pkt :: Either String SecretKey]+ ] of+ (sk : _) -> pure sk+ [] ->+ assertFailure+ "v6-secret.pgp.aa should contain a secret key packet"+ >> fail "expected secret key packet"+ originalSKey <-+ case ska of+ SUUnencrypted skey _ -> pure skey+ _ ->+ assertFailure+ "v6-secret.pgp.aa should contain unencrypted secret key material"+ >> fail "expected unencrypted secret key"+ changed <-+ case encryptPrivateKeyWithPolicyAndSaltAndIV+ defaultPolicy+ pkp+ (Salt "1234567890ABCDEF")+ (IV "1234567890ABCDE")+ ska+ passphrase of+ Left err ->+ assertFailure ("v6 secret key encryption failed: " ++ err)+ >> fail "v6 secret key encryption failed"+ Right x -> pure x+ case changed of+ SUSAEAD AES256 OCB (Argon2 _ t p em) _ encryptedPayload -> do+ assertEqual+ "v6 secret key encryption should use expected Argon2 t parameter"+ 1+ t+ assertEqual+ "v6 secret key encryption should use expected Argon2 p parameter"+ 4+ p+ assertEqual+ "v6 secret key encryption should use expected Argon2 encoded memory parameter"+ 15+ em+ assertBool+ "v6 secret key encryption should emit encrypted payload"+ (not (BL.null encryptedPayload))+ _ ->+ assertFailure+ "v6 secret key encryption should emit SUSAEAD/AES256/OCB with Argon2 S2K"+ let serialized = runPut (put (toPkt (SecretKey pkp changed)))+ reparsed = parsePkts serialized+ (parsedPKP, parsedSKA) <-+ case reparsed of+ (SecretKeyPkt pkpayload skaddendum : _) -> pure (pkpayload, skaddendum)+ _ ->+ assertFailure+ "re-serialized v6 secret key should parse back as a secret key packet"+ >> fail "expected serialized secret key packet"+ decrypted <-+ case decryptPrivateKey (parsedPKP, parsedSKA) passphrase of+ Left err ->+ assertFailure+ ("v6 secret key roundtrip decryption failed: " ++ err)+ >> fail "v6 secret key roundtrip decryption failed"+ Right x -> pure x+ case decrypted of+ SUUnencrypted skey _ ->+ assertEqual+ "v6 secret key roundtrip should preserve secret key material"+ originalSKey+ skey+ _ ->+ assertFailure+ "v6 secret key roundtrip should decrypt to unencrypted secret material"++testV4SecretKeyAEADOCBRoundTripCompat :: Assertion+testV4SecretKeyAEADOCBRoundTripCompat = do+ (pkp, privateKey) <- loadUnencryptedRsaSigner+ let skey = RSAPrivateKey (RSA_PrivateKey privateKey)+ passphrase = "legacy-aead-passphrase"+ compatPolicy =+ (policyForRFC RFC4880)+ { policySecretKeyProtection =+ policySecretKeyProtection defaultPolicy+ }+ salt = Salt (B.pack [0x00 .. 0x0f])+ iv = IV (B.pack [0x10 .. 0x1e])+ encrypted <-+ case encryptPrivateKeyWithPolicyAndSaltAndIV+ compatPolicy+ pkp+ salt+ iv+ (SUUnencrypted skey 0)+ passphrase of+ Left err ->+ assertFailure ("v4 AEAD secret key encryption failed: " ++ err)+ >> fail "v4 AEAD secret key encryption failed"+ Right x -> pure x+ case encrypted of+ SUSAEAD AES256 OCB (Argon2 _ t p em) _ encryptedPayload -> do+ assertEqual+ "v4 AEAD secret key encryption should use expected Argon2 t"+ 1+ t+ assertEqual+ "v4 AEAD secret key encryption should use expected Argon2 p"+ 4+ p+ assertEqual+ "v4 AEAD secret key encryption should use expected Argon2 encoded-memory"+ 15+ em+ assertBool+ "v4 AEAD secret key encryption should emit encrypted payload"+ (not (BL.null encryptedPayload))+ _ ->+ assertFailure+ "v4 secret key encryption should emit SUSAEAD/AES256/OCB with Argon2 S2K"+ let serialized = runPut (put (toPkt (SecretKey pkp encrypted)))+ reparsed = parsePkts serialized+ (parsedPKP, parsedSKA) <-+ case reparsed of+ (SecretKeyPkt pkpayload skaddendum : _) -> pure (pkpayload, skaddendum)+ _ ->+ assertFailure+ "re-serialized v4 AEAD secret key should parse back as a secret key packet"+ >> fail "expected serialized secret key packet"+ decrypted <-+ case decryptPrivateKey (parsedPKP, parsedSKA) passphrase of+ Left err ->+ assertFailure+ ("v4 AEAD secret key roundtrip decryption failed: " ++ err)+ >> fail "v4 AEAD secret key roundtrip decryption failed"+ Right x -> pure x+ case decrypted of+ SUUnencrypted skey' _ ->+ assertEqual+ "v4 AEAD secret key roundtrip should preserve secret key material"+ skey+ skey'+ _ ->+ assertFailure+ "v4 AEAD secret key roundtrip should decrypt to unencrypted secret material"++testSUSymSecretKeyRoundTrip :: Assertion+testSUSymSecretKeyRoundTrip = do+ passphrase <- readPKIPassphrase+ packets <-+ DC.runConduitRes $+ CB.sourceFile "tests/data/unencrypted.seckey"+ DC..| conduitGet get+ DC..| CL.consume+ (pkp, skey) <-+ case packets of+ (SecretKeyPkt pkpayload (SUUnencrypted sk _) : _) -> pure (pkpayload, sk)+ _ ->+ assertFailure+ "unencrypted.seckey did not begin with an unencrypted secret key packet"+ >> fail "expected unencrypted secret key packet"+ let cleartext = legacyRsaSecretKeyBytes skey+ cleartextLBS = BL.fromStrict cleartext+ clearWithChecksum =+ cleartextLBS <> runPut (putWord16be (legacyChecksum16 cleartext))+ iv = IV (B.pack [0 .. 15])+ sa = AES128+ keyMaterial <-+ case string2Key (Simple DeprecatedMD5) 16 passphrase of+ Left err ->+ assertFailure+ ("failed to derive SUSym key material: " ++ renderS2KError err)+ >> fail "failed to derive SUSym key material"+ Right km -> pure km+ encrypted <-+ case encryptNoNonce+ sa+ (Simple DeprecatedMD5)+ iv+ (BL.toStrict clearWithChecksum)+ keyMaterial of+ Left err ->+ assertFailure+ ("failed to encrypt legacy SUSym secret key: " ++ show err)+ >> fail "failed to encrypt legacy SUSym secret key"+ Right bs -> pure bs+ let legacySka = SUSym sa iv (BL.fromStrict encrypted)+ serialized = runPut (put (toPkt (SecretKey pkp legacySka)))+ reparsed = parsePkts serialized+ (parsedPKP, parsedSKA) <-+ case reparsed of+ (SecretKeyPkt pkpayload skaddendum : _) -> pure (pkpayload, skaddendum)+ _ ->+ assertFailure+ "serialized SUSym secret key should parse back as a secret key packet"+ >> fail "expected serialized secret key packet"+ decrypted <-+ case decryptPrivateKey (parsedPKP, parsedSKA) passphrase of+ Left err ->+ assertFailure ("SUSym secret key decryption failed: " ++ err)+ >> fail "SUSym secret key decryption failed"+ Right x -> pure x+ case decrypted of+ SUUnencrypted parsedSKey _ ->+ assertEqual+ "SUSym secret key roundtrip should preserve secret key material"+ skey+ parsedSKey+ _ ->+ assertFailure+ "SUSym secret key roundtrip should decrypt to unencrypted secret material"++legacyRsaSecretKeyBytes :: SKey -> B.ByteString+legacyRsaSecretKeyBytes (RSAPrivateKey (RSA_PrivateKey (RSA.PrivateKey _ d p q _ _ _))) =+ BL.toStrict $+ runPut $+ case inverse q p of+ Just u -> do+ put (MPI d)+ put (MPI p)+ put (MPI q)+ put (MPI u)+ Nothing -> error "invalid RSA key in legacy secret key test"+legacyRsaSecretKeyBytes _ = error "legacyRsaSecretKeyBytes requires an RSA secret key"++legacyChecksum16 :: B.ByteString -> Word16+legacyChecksum16 =+ fromIntegral+ . B.foldl'+ (\acc octet -> (acc + fromIntegral octet) `mod` (65536 :: Integer))+ 0++testLegacyEncryptionFixtureShape :: Assertion+testLegacyEncryptionFixtureShape = do+ packets <-+ DC.runConduitRes $+ CB.sourceFile "tests/data/encryption.gpg"+ DC..| conduitGet get+ DC..| CL.consume+ let hasKeyEncapsulation = any isKeyEncapsulation packets+ hasEncryptedData = any isEncryptedData packets+ assertBool+ "encryption.gpg should contain PKESK or SKESK key-encapsulation"+ hasKeyEncapsulation+ assertBool+ "encryption.gpg should contain encrypted data"+ hasEncryptedData+ where+ isKeyEncapsulation (SKESKPkt _) = True+ isKeyEncapsulation (PKESKPkt _) = True+ isKeyEncapsulation _ = False+ isEncryptedData SymEncDataPkt {} = True+ isEncryptedData SymEncIntegrityProtectedDataPkt {} = True+ isEncryptedData _ = False++testBuildPKESKPayloadForRecipientRSA :: Assertion+testBuildPKESKPayloadForRecipientRSA = do+ (baseRecipient, privateKey) <- loadUnencryptedRsaSigner+ let recipient = setKeyVersion V6 baseRecipient+ sessionKey = SessionKey (B.replicate 32 0x2e)+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk builder rsa payload"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ =+ pure+ ( Just+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Nothing+ , pkeskRecipientSKey = RSAPrivateKey (RSA_PrivateKey privateKey)+ }+ )+ sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey+ pkeskPayloadResult <-+ buildPKESKPayloadForRecipient PreferV6 recipient sessionMaterial+ pkeskPayload <-+ case pkeskPayloadResult of+ Left err ->+ assertFailure+ ("buildPKESKPayloadForRecipient failed: " ++ show err)+ >> fail "buildPKESKPayloadForRecipient failed"+ Right p -> pure p+ case pkeskPayload of+ PKESKPayloadV6Packet (PKESKPayloadV6 _ RSA eskBytesLazy) -> do+ let eskBytes = BL.toStrict eskBytesLazy+ assertBool+ "PKESKv6 RSA ESK should include MPI framing"+ (B.length eskBytes >= 2)+ let mpiBits =+ fromIntegral (B.index eskBytes 0) * 256+ + fromIntegral (B.index eskBytes 1)+ mpiLen = (mpiBits + 7) `div` 8+ assertEqual+ "PKESKv6 RSA ESK should be exactly one RFC9580 MPI"+ (2 + mpiLen)+ (B.length eskBytes)+ other ->+ assertFailure+ ("Expected PKESKv6 RSA payload, got " ++ show other)+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ PKESKPkt pkeskPayload+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithPKESKContext+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "PKESK builder RSA decrypt payload"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testBuildPKESKv3PayloadForRecipientRSAInterop :: Assertion+testBuildPKESKv3PayloadForRecipientRSAInterop = do+ (baseRecipient, privateKey) <- loadUnencryptedRsaSigner+ let recipient = setKeyVersion V4 baseRecipient+ sessionKey = SessionKey (B.replicate 32 0x2d)+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk builder rsa v3 interop payload"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyCallback _ = pure (Just (RSAPrivateKey (RSA_PrivateKey privateKey)))+ keyContextCallback pkt = do+ msk <- keyCallback pkt+ pure $+ fmap+ ( \sk ->+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Nothing+ , pkeskRecipientSKey = sk+ }+ )+ msk+ sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey+ pkeskPayloadResult <-+ buildPKESKv3PayloadForRecipient+ recipient+ (pkeskV3SessionMaterial sessionMaterial)+ pkeskPayload <-+ case pkeskPayloadResult of+ Left err ->+ assertFailure+ ("buildPKESKv3PayloadForRecipient failed: " ++ show err)+ >> fail "buildPKESKv3PayloadForRecipient failed"+ Right p@(PKESKPayloadV3Packet (PKESKPayloadV3 _ _ RSA _)) -> pure p+ Right other ->+ assertFailure ("Expected PKESK3 RSA payload, got " ++ show other)+ >> fail "Unexpected PKESK payload variant"+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ PKESKPkt pkeskPayload+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "PKESKv3 RSA interop decrypt payload"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testBuildPKESKv3PayloadForRecipientSupportsV6Key :: Assertion+testBuildPKESKv3PayloadForRecipientSupportsV6Key = do+ (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner+ let recipient = setKeyVersion V6 baseRecipient+ sessionKey = SessionKey (B.replicate 32 0x11)+ let expectedKeyId =+ EightOctetKeyId+ (BL.take 8 (unFingerprint (fingerprint recipient)))+ sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey+ result <-+ buildPKESKv3PayloadForRecipient+ recipient+ (pkeskV3SessionMaterial sessionMaterial)+ case result of+ Right (PKESKPayloadV3Packet (PKESKPayloadV3 _ keyId RSA _)) ->+ assertEqual+ "PKESKv3 builder should derive v6 key ID from the high-order 64 bits of the fingerprint"+ expectedKeyId+ keyId+ Right payload ->+ assertFailure+ ( "Expected PKESK3 RSA payload for v6 recipient, got "+ ++ show payload+ )+ Left err ->+ assertFailure+ ( "Expected PKESK3 RSA payload for v6 recipient, got error "+ ++ show err+ )++testBuildPKESKv3PayloadForRecipientECDHInterop :: Assertion+testBuildPKESKv3PayloadForRecipientECDHInterop = do+ let (recipientPub, recipientPriv) = syntheticNISTP256Low+ recipient =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ ECDH+ ( ECDHPubKey+ (ECDSAPubKey (ECDSA_PublicKey recipientPub))+ SHA256+ AES128+ )+ sessionKey = SessionKey (B.replicate 32 0x21)+ salt = Salt (B.pack [0x20 .. 0x3f])+ payload = "pkesk builder ecdh v3 interop payload"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ recipientSKey = ECDHPrivateKey (ECDSA_PrivateKey recipientPriv)+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipient+ , pkeskRecipientSKey = recipientSKey+ }+ )+ )+ sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey+ pkeskPayloadResult <-+ buildPKESKv3PayloadForRecipient+ recipient+ (pkeskV3SessionMaterial sessionMaterial)+ pkeskPayload <-+ case pkeskPayloadResult of+ Left err ->+ assertFailure+ ("buildPKESKv3PayloadForRecipient failed: " ++ show err)+ >> fail "buildPKESKv3PayloadForRecipient failed"+ Right p@(PKESKPayloadV3Packet (PKESKPayloadV3 _ _ ECDH _)) -> pure p+ Right other ->+ assertFailure+ ("Expected PKESK3 ECDH payload, got " ++ show other)+ >> fail "Unexpected PKESK payload variant"+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ PKESKPkt pkeskPayload+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "PKESKv3 ECDH interop decrypt payload"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testPKESKv3ECDHWireFormat :: Assertion+testPKESKv3ECDHWireFormat = do+ let (recipientPub, _) = syntheticNISTP256Low+ recipient =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ ECDH+ ( ECDHPubKey+ (ECDSAPubKey (ECDSA_PublicKey recipientPub))+ SHA256+ AES256+ )+ -- Use a session key whose first byte is 0x00 so leading-zero stripping+ -- is observable if i2osp truncates the wrapped key.+ sessionKey = SessionKey (B.cons 0x00 (B.replicate 31 0xAB))+ sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey+ pkeskResult <-+ buildPKESKv3PayloadForRecipient+ recipient+ (pkeskV3SessionMaterial sessionMaterial)+ case pkeskResult of+ Left err ->+ assertFailure+ ("buildPKESKv3PayloadForRecipient failed: " ++ show err)+ Right p -> do+ -- Serialise the packet to wire bytes then parse the PKESK body manually.+ let wire = BL.toStrict (runPut (put (PKESKPkt p)))+ -- Skip new-format tag byte (0xC1), then parse the packet length.+ -- For the test we just round-trip through parsePkts and check the body layout.+ case parsePkts (BL.fromStrict wire) of+ [ PKESKPkt+ ( PKESKPayloadV3Packet+ (PKESKPayloadV3 _ _ ECDH (ephMPI NE.:| [wrappedMPI]))+ )+ ] -> do+ -- The ephemeral MPI should be non-trivially large (EC point).+ assertBool "ephemeral MPI should be non-empty" (unMPI ephMPI > 0)+ -- The wrapped MPI stores the raw i2osp bytes; for AES-256 the RFC 3394+ -- ciphertext is 48 bytes. i2osp strips leading zeros so the stored+ -- integer may represent fewer bytes, but candidateWrappedRFC3394Ciphertexts+ -- handles that. What we're checking here is the WIRE format:+ -- after the ephemeral MPI there must be exactly 1-byte-len + C bytes,+ -- not a second MPI header.+ --+ -- Re-serialise and inspect the bytes after the ephemeral MPI manually.+ let ephBytes = BL.toStrict (runPut (put ephMPI))+ pktBody =+ -- wire: 0xC1 | varlen | version(1) | keyid(8) | pka(1) | body+ -- skip tag(1) + len(variable) + version(1) + keyid(8) + pka(1)+ -- We just serialise the body directly via putPKESKv3SessionKeyMaterial equivalent:+ BL.toStrict+ ( runPut+ ( put+ ( PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId (BL.replicate 8 0))+ ECDH+ (ephMPI NE.:| [wrappedMPI])+ )+ )+ )+ )+ )+ -- Find the position of the body after tag+len+version+keyid+pka.+ -- Tag: 1 byte; new-format len: 1 or 2 bytes; version: 1; keyid: 8; pka: 1 = 12 or 13 bytes+ -- Instead of parsing the header, just verify the body doesn't start with+ -- the two-byte MPI bit-count of the wrapped key.+ -- After ephBytes in the body, the next byte should be the 1-byte count (24-48),+ -- not the high byte of an MPI bit-count (which would be 0x01 for 383-bit keys).+ -- We search for ephBytes in pktBody and check what follows.+ let stripped = dropPKESKv3Header pktBody+ afterEph = B.drop (B.length ephBytes) stripped+ assertBool+ "PKESKv3 ECDH wire body: after ephemeral MPI there must be at least 1 byte for wrapped-key length"+ (not (B.null afterEph))+ let wrappedLenByte = fromIntegral (B.head afterEph) :: Int+ assertBool+ ( "PKESKv3 ECDH wire: wrapped-key length byte "+ ++ show wrappedLenByte+ ++ " should be a valid RFC 3394 wrapped-key length (24, 32, 40, or 48)"+ )+ (wrappedLenByte `elem` [24, 32, 40, 48])+ assertEqual+ "PKESKv3 ECDH wire: bytes after length field must equal length"+ wrappedLenByte+ (B.length (B.tail afterEph))+ other ->+ assertFailure+ ( "Expected single PKESKv3 ECDH packet after round-trip, got "+ ++ show other+ )+ where+ -- Drop the new-format packet header (tag byte + variable-length field ++ -- version byte + 8-byte key-id + pka byte) to reach the session-key material.+ dropPKESKv3Header bs+ | B.length bs < 3 = bs+ | otherwise =+ let lenByte = B.index bs 1+ headerLen+ | lenByte < 192 = 2 -- 1-byte length+ | lenByte < 224 = 3 -- 2-byte length+ | otherwise = 6 -- 5-byte length (rare)+ in B.drop (headerLen + 1 + 8 + 1) bs++testBuildPKESKv3PayloadForRecipientCurve25519LegacyInterop+ :: Assertion+testBuildPKESKv3PayloadForRecipientCurve25519LegacyInterop = do+ let recipientSecretRaw = B.pack [1 .. 32]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ( "failed to initialize Curve25519Legacy recipient secret key: "+ ++ show err+ )+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ recipient =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ ECDH+ ( ECDHPubKey+ ( EdDSAPubKey+ EdSigningCurve25519+ ( PrefixedNativeEPoint+ (EPoint (os2ip (B.cons 0x40 recipientPublicRaw)))+ )+ )+ SHA256+ AES256+ )+ recipientSKey =+ ECDHPrivateKey+ ( ECDSA_PrivateKey+ ( ECDSA.PrivateKey+ (ECCT.getCurveByName ECCT.SEC_p256r1)+ (os2ip recipientSecretRaw)+ )+ )+ sessionKey = SessionKey (B.replicate 32 0x22)+ salt = Salt (B.pack [0x40 .. 0x5f])+ payload = "pkesk builder curve25519legacy v3 interop payload"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipient+ , pkeskRecipientSKey = recipientSKey+ }+ )+ )+ sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey+ pkeskPayloadResult <-+ buildPKESKv3PayloadForRecipient+ recipient+ (pkeskV3SessionMaterial sessionMaterial)+ pkeskPayload <-+ case pkeskPayloadResult of+ Left err ->+ assertFailure+ ("buildPKESKv3PayloadForRecipient failed: " ++ show err)+ >> fail "buildPKESKv3PayloadForRecipient failed"+ Right+ p@( PKESKPayloadV3Packet+ (PKESKPayloadV3 _ _ ECDH (ephemeralMPI NE.:| _))+ ) -> do+ let ephemeralBytes = i2osp (unMPI ephemeralMPI)+ assertEqual+ "PKESKv3 Curve25519Legacy ephemeral point should be 0x40-prefixed 33-octet value"+ 33+ (B.length ephemeralBytes)+ assertEqual+ "PKESKv3 Curve25519Legacy ephemeral point should use RFC6637 0x40 prefix"+ 0x40+ (B.head ephemeralBytes)+ pure p+ Right other ->+ assertFailure+ ("Expected PKESK3 ECDH payload, got " ++ show other)+ >> fail "Unexpected PKESK payload variant"+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ PKESKPkt pkeskPayload+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "PKESKv3 Curve25519Legacy interop decrypt payload"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testBuildPKESKv3PayloadForRecipientRejectsCurve448Legacy+ :: Assertion+testBuildPKESKv3PayloadForRecipientRejectsCurve448Legacy = do+ let recipient =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ ECDH+ ( ECDHPubKey+ (EdDSAPubKey EdSigningCurve448 (PrefixedNativeEPoint (EPoint 1)))+ SHA512+ AES256+ )+ sessionKey = SessionKey (B.replicate 32 0x23)+ sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey+ result <-+ buildPKESKv3PayloadForRecipient+ recipient+ (pkeskV3SessionMaterial sessionMaterial)+ case result of+ Left (InvalidRecipientKeyMaterial ECDH err)+ | "RFC6637-compatible" `isInfixOf` err -> pure ()+ | otherwise ->+ assertFailure+ ("Expected RFC6637 compatibility rejection, got: " ++ err)+ Left err ->+ assertFailure+ ("Expected InvalidRecipientKeyMaterial ECDH, got " ++ show err)+ Right payload ->+ assertFailure+ ( "Expected RFC6637 compatibility rejection, got payload: "+ ++ show payload+ )++testBuildPKESKv6PayloadForRecipientX25519RawKeySemantics+ :: Assertion+testBuildPKESKv6PayloadForRecipientX25519RawKeySemantics = do+ let recipientSecretRaw = B.pack [1 .. 32]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient secret key: " ++ show err)+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ v4Recipient =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip recipientPublicRaw)))+ )+ recipient = setKeyVersion V6 v4Recipient+ sessionKey = SessionKey (B.replicate 32 0x24)+ sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey+ pkeskPayloadResult <-+ buildPKESKPayloadForRecipient PreferV6 recipient sessionMaterial+ case pkeskPayloadResult of+ Left err ->+ assertFailure+ ( "buildPKESKPayloadForRecipient failed for v6 X25519: "+ ++ show err+ )+ Right+ (PKESKPayloadV6Packet (PKESKPayloadV6 _ X25519 eskBytesLazy)) -> do+ let eskBytes = BL.toStrict eskBytesLazy+ assertX25519EskShape "v6 X25519 raw-key conformance" eskBytes+ let wrappedLen = fromIntegral (B.index eskBytes 32) :: Int+ assertEqual+ "v6 X25519 wrapped session key (AES-KW RFC3394 of raw 32 bytes) must be 40 bytes"+ 40+ wrappedLen+ Right other ->+ assertFailure+ ("Expected PKESKPayloadV6 with X25519, got " ++ show other)++{- | Conformance test: v6 X448 PKESK uses raw session key, not v3-encoded material.+RFC 9580 specifies that v6 X448 wraps the raw session key directly (no algorithm byte or checksum).+When the raw 32-byte key is AES-KW wrapped with RFC3394: 32 bytes + 8-byte integrity = 40 bytes.+If v3-encoded (1 byte algo + 32 bytes key + 2 bytes checksum + ~5 bytes padding = 40 bytes unwrapped),+the wrapped result would be ~48 bytes, so we verify wrapped size is 40.+-}+testBuildPKESKv6PayloadForRecipientX448RawKeySemantics+ :: Assertion+testBuildPKESKv6PayloadForRecipientX448RawKeySemantics = do+ let recipientSecretRaw = B.pack [1 .. 56]+ recipientSecret =+ case CE.eitherCryptoError (C448.secretKey recipientSecretRaw) of+ Left err ->+ error+ ("failed to initialize X448 recipient secret key: " ++ show err)+ Right sk -> sk+ recipientPublicRaw = BA.convert (C448.toPublic recipientSecret) :: B.ByteString+ v4Recipient =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X448+ ( EdDSAPubKey+ EdSigningCurve448+ (NativeEPoint (EPoint (os2ip recipientPublicRaw)))+ )+ recipient = setKeyVersion V6 v4Recipient+ sessionKey = SessionKey (B.replicate 32 0x25)+ sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey+ pkeskPayloadResult <-+ buildPKESKPayloadForRecipient PreferV6 recipient sessionMaterial+ case pkeskPayloadResult of+ Left err ->+ assertFailure+ ("buildPKESKPayloadForRecipient failed for v6 X448: " ++ show err)+ Right (PKESKPayloadV6Packet (PKESKPayloadV6 _ X448 eskBytesLazy)) -> do+ let eskBytes = BL.toStrict eskBytesLazy+ assertX448EskShape "v6 X448 raw-key conformance" eskBytes+ let wrappedLen = fromIntegral (B.index eskBytes 56) :: Int+ assertEqual+ "v6 X448 wrapped session key (AES-KW RFC3394 of raw 32 bytes) must be 40 bytes"+ 40+ wrappedLen+ Right other ->+ assertFailure+ ("Expected PKESKPayloadV6 with X448, got " ++ show other)++testBuildPKESKPayloadForRecipientWithPolicyForceV3Interop+ :: Assertion+testBuildPKESKPayloadForRecipientWithPolicyForceV3Interop = do+ (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner+ let recipient = setKeyVersion V4 baseRecipient+ sessionKey = SessionKey (B.replicate 32 0x16)+ sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey+ result <-+ buildPKESKPayloadForRecipient+ ForceV3Interop+ recipient+ sessionMaterial+ case result of+ Right (PKESKPayloadV3Packet (PKESKPayloadV3 _ _ RSA _)) -> pure ()+ Right payload ->+ assertFailure+ ("Expected PKESK3 RSA for ForceV3Interop, got " ++ show payload)+ Left err ->+ assertFailure+ ("Expected PKESK3 RSA for ForceV3Interop, got error " ++ show err)++testRecipientVersionStrategyForProfileHonorsHints :: Assertion+testRecipientVersionStrategyForProfileHonorsHints = do+ (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner+ let recipient = setKeyVersion V6 baseRecipient+ case recipientVersionStrategyForProfile+ EncryptStrictDefault+ ( recipientEncryptionTargetWithStrategy+ recipient+ RecipientForceV3Interop+ ) of+ Right RecipientForceV3Interop -> pure ()+ other ->+ assertFailure+ ("Expected explicit recipient hint to win, got " ++ show other)+ case recipientVersionStrategyForProfile+ EncryptInteropLegacy+ (recipientEncryptionTarget recipient) of+ Right RecipientForceV3Interop -> pure ()+ other ->+ assertFailure+ ( "Expected legacy profile fallback to force v3, got "+ ++ show other+ )+ let v4Recipient = setKeyVersion V4 recipient+ case recipientVersionStrategyForProfile+ EncryptStrictDefault+ (recipientEncryptionTarget recipient) of+ Right RecipientPreferV6 -> pure ()+ other ->+ assertFailure+ ( "Expected strict profile auto-detect to prefer v6 for v6 recipients, got "+ ++ show other+ )+ case recipientVersionStrategyForProfile+ EncryptStrictDefault+ (recipientEncryptionTarget v4Recipient) of+ Right RecipientForceV3Interop -> pure ()+ other ->+ assertFailure+ ( "Expected strict profile auto-detect to force v3 for legacy recipients, got "+ ++ show other+ )++testEncryptRecipientsWithRequestAutoDetectsMixedRecipientStrategies+ :: Assertion+testEncryptRecipientsWithRequestAutoDetectsMixedRecipientStrategies = do+ (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner+ let v4Recipient = setKeyVersion V4 baseRecipient+ v6Recipient = setKeyVersion V6 baseRecipient+ request =+ RecipientEncryptRequest+ { recipientEncryptRequestTargets =+ [ recipientEncryptionTarget v6Recipient+ , recipientEncryptionTarget v4Recipient+ ]+ , recipientEncryptRequestPayloadShape =+ defaultRecipientPayloadShape+ , recipientEncryptRequestPayload = "recipient autodetect"+ , recipientEncryptRequestSymmetricOverride = Just AES256+ , recipientEncryptRequestOverrides =+ RecipientEncryptRequestSEIPDv2Overrides+ { recipientEncryptRequestAEADOverride = Just OCB+ , recipientEncryptRequestChunkSizeOverride = Just 6+ , recipientEncryptRequestSaltOverride =+ Just (Salt (B.replicate 32 0x77))+ }+ }+ result <- encryptForRecipients request+ case result of+ Left err ->+ assertFailure+ ( "Expected mixed-recipient request to encrypt successfully, got "+ ++ show err+ )+ Right RecipientEncryptResult {recipientEncryptPackets = packets} -> do+ let pkesks = [p | PKESKPkt p <- packets]+ assertBool+ "strict profile auto-detect should emit PKESKv6 for v6 recipients"+ (any isPKESK6 pkesks)+ assertBool+ "strict profile auto-detect should emit PKESKv3 for v4 recipients"+ (any isPKESK3 pkesks)+ where+ isPKESK6 PKESKPayloadV6Packet {} = True+ isPKESK6 _ = False+ isPKESK3 PKESKPayloadV3Packet {} = True+ isPKESK3 _ = False++testEncryptRecipientsNegotiatesSymmetricAlgorithmWhenEnabled+ :: Assertion+testEncryptRecipientsNegotiatesSymmetricAlgorithmWhenEnabled = 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 =+ [AES128, AES256]+ , recipientCapabilityPreferredAEADAlgorithms = [OCB]+ }+ v4Caps =+ RecipientCapabilities+ { recipientCapabilityKeyVersion = V4+ , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient+ , recipientCapabilityKeyFlags = Set.empty+ , recipientCapabilityFeatures = Set.empty+ , recipientCapabilityPreferredSymmetricAlgorithms = [AES128]+ , recipientCapabilityPreferredAEADAlgorithms = [OCB]+ }+ request =+ RecipientEncryptRequest+ { recipientEncryptRequestTargets =+ [ recipientEncryptionTargetWithCapabilities v6Recipient v6Caps+ , recipientEncryptionTargetWithCapabilities v4Recipient v4Caps+ ]+ , recipientEncryptRequestPayloadShape =+ defaultRecipientPayloadShape+ , recipientEncryptRequestPayload =+ "recipient capability negotiation payload"+ , recipientEncryptRequestSymmetricOverride = Nothing+ , recipientEncryptRequestOverrides =+ RecipientEncryptRequestSEIPDv2Overrides+ { recipientEncryptRequestAEADOverride = Just OCB+ , recipientEncryptRequestChunkSizeOverride = Just 6+ , recipientEncryptRequestSaltOverride =+ Just (Salt (B.replicate 32 0x22))+ }+ }+ result <-+ encryptForRecipientsWithCapabilityNegotiation+ RecipientCapabilityNegotiationOn+ request+ case result of+ Left err ->+ assertFailure+ ( "Expected recipient capability negotiation to succeed, got "+ ++ show err+ )+ Right+ RecipientEncryptResult+ { recipientEncryptSessionMaterial = material+ } ->+ assertEqual+ "capability negotiation should pick common preferred symmetric algorithm"+ AES128+ (pkeskSessionAlgorithm material)++testEncryptRecipientsNegotiatesSymmetricAlgorithmByDefault+ :: Assertion+testEncryptRecipientsNegotiatesSymmetricAlgorithmByDefault = 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 =+ [AES128, AES256]+ , recipientCapabilityPreferredAEADAlgorithms = [OCB]+ }+ v4Caps =+ RecipientCapabilities+ { recipientCapabilityKeyVersion = V4+ , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient+ , recipientCapabilityKeyFlags = Set.empty+ , recipientCapabilityFeatures = Set.empty+ , recipientCapabilityPreferredSymmetricAlgorithms = [AES128]+ , recipientCapabilityPreferredAEADAlgorithms = [OCB]+ }+ request =+ RecipientEncryptRequest+ { recipientEncryptRequestTargets =+ [ recipientEncryptionTargetWithCapabilities v6Recipient v6Caps+ , recipientEncryptionTargetWithCapabilities v4Recipient v4Caps+ ]+ , recipientEncryptRequestPayloadShape =+ defaultRecipientPayloadShape+ , recipientEncryptRequestPayload =+ "default recipient capability negotiation payload"+ , recipientEncryptRequestSymmetricOverride = Nothing+ , recipientEncryptRequestOverrides =+ RecipientEncryptRequestSEIPDv2Overrides+ { recipientEncryptRequestAEADOverride = Just OCB+ , recipientEncryptRequestChunkSizeOverride = Just 6+ , recipientEncryptRequestSaltOverride =+ Just (Salt (B.replicate 32 0x24))+ }+ }+ result <- encryptForRecipients request+ case result of+ Left err ->+ assertFailure+ ( "Expected default encryptForRecipients negotiation to succeed, got "+ ++ show err+ )+ Right+ RecipientEncryptResult+ { recipientEncryptSessionMaterial = material+ } ->+ assertEqual+ "encryptForRecipients should negotiate recipient capabilities by default"+ 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 =+ [AES128, AES256]+ , recipientCapabilityPreferredAEADAlgorithms = [OCB]+ }+ v4Caps =+ RecipientCapabilities+ { recipientCapabilityKeyVersion = V4+ , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient+ , recipientCapabilityKeyFlags = Set.empty+ , recipientCapabilityFeatures = Set.empty+ , recipientCapabilityPreferredSymmetricAlgorithms = [AES128]+ , recipientCapabilityPreferredAEADAlgorithms = [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+ (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 = [AES128]+ , recipientCapabilityPreferredAEADAlgorithms = [OCB]+ }+ v4Caps =+ RecipientCapabilities+ { recipientCapabilityKeyVersion = V4+ , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient+ , recipientCapabilityKeyFlags = Set.empty+ , recipientCapabilityFeatures = Set.empty+ , recipientCapabilityPreferredSymmetricAlgorithms = [AES256]+ , recipientCapabilityPreferredAEADAlgorithms = [OCB]+ }+ request =+ RecipientEncryptRequest+ { recipientEncryptRequestTargets =+ [ recipientEncryptionTargetWithCapabilities v6Recipient v6Caps+ , recipientEncryptionTargetWithCapabilities v4Recipient v4Caps+ ]+ , recipientEncryptRequestPayloadShape =+ defaultRecipientPayloadShape+ , recipientEncryptRequestPayload =+ "recipient capability mismatch payload"+ , recipientEncryptRequestSymmetricOverride = Nothing+ , recipientEncryptRequestOverrides =+ RecipientEncryptRequestSEIPDv2Overrides+ { recipientEncryptRequestAEADOverride = Just OCB+ , recipientEncryptRequestChunkSizeOverride = Just 6+ , recipientEncryptRequestSaltOverride =+ Just (Salt (B.replicate 32 0x23))+ }+ }+ result <-+ encryptForRecipientsWithCapabilityNegotiation+ RecipientCapabilityNegotiationOn+ request+ case result of+ Left+ ( RecipientCapabilitySelectionFailure+ (RecipientCapabilityNoCommonSymmetricAlgorithms _)+ ) -> pure ()+ Left other ->+ assertFailure+ ( "Expected RecipientCapabilityNoCommonSymmetricAlgorithms, got "+ ++ show other+ )+ Right _ ->+ assertFailure+ "Expected recipient capability negotiation to fail without common symmetric algorithm"++testRecipientCapabilitiesFromSubpacketPayloadsExtractsPreferredAEADAlgorithms+ :: Assertion+testRecipientCapabilitiesFromSubpacketPayloadsExtractsPreferredAEADAlgorithms = do+ (recipient, _privateKey) <- loadUnencryptedRsaSigner+ let caps =+ recipientCapabilitiesFromSubpacketPayloads+ recipient+ [OtherSigSub 39 (BL.pack [9, 2, 7, 3, 9, 2])]+ assertEqual+ "preferred AEAD ciphersuites subpacket should extract unique AEAD preferences in declaration order"+ [OCB, GCM]+ (recipientCapabilityPreferredAEADAlgorithms caps)++testEncryptRecipientsNegotiatesAEADAlgorithmWhenEnabled+ :: Assertion+testEncryptRecipientsNegotiatesAEADAlgorithmWhenEnabled = 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.fromList [FeatureSEIPDv1, FeatureSEIPDv2]+ , recipientCapabilityPreferredSymmetricAlgorithms =+ [AES128, AES256]+ , recipientCapabilityPreferredAEADAlgorithms = [GCM, OCB]+ }+ v4Caps =+ RecipientCapabilities+ { recipientCapabilityKeyVersion = V4+ , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient+ , recipientCapabilityKeyFlags = Set.empty+ , recipientCapabilityFeatures =+ Set.fromList [FeatureSEIPDv1, FeatureSEIPDv2]+ , recipientCapabilityPreferredSymmetricAlgorithms = [AES128]+ , recipientCapabilityPreferredAEADAlgorithms = [GCM]+ }+ request =+ RecipientEncryptRequest+ { recipientEncryptRequestTargets =+ [ recipientEncryptionTargetWithCapabilities v6Recipient v6Caps+ , recipientEncryptionTargetWithCapabilities v4Recipient v4Caps+ ]+ , recipientEncryptRequestPayloadShape =+ defaultRecipientPayloadShape+ , recipientEncryptRequestPayload =+ "recipient AEAD capability negotiation payload"+ , recipientEncryptRequestSymmetricOverride = Nothing+ , recipientEncryptRequestOverrides =+ RecipientEncryptRequestSEIPDv2Overrides+ { recipientEncryptRequestAEADOverride = Nothing+ , recipientEncryptRequestChunkSizeOverride = Just 6+ , recipientEncryptRequestSaltOverride =+ Just (Salt (B.replicate 32 0x26))+ }+ }+ result <-+ encryptForRecipientsWithCapabilityNegotiation+ RecipientCapabilityNegotiationOn+ request+ case result of+ Left err ->+ assertFailure+ ( "Expected AEAD capability negotiation to succeed, got "+ ++ show err+ )+ Right+ RecipientEncryptResult+ { recipientEncryptPackets = packets+ , recipientEncryptSessionMaterial = material+ } -> do+ assertEqual+ "AEAD capability negotiation should preserve symmetric negotiation"+ AES128+ (pkeskSessionAlgorithm material)+ case [ aa+ | SymEncIntegrityProtectedDataPkt (SEIPD2 _ aa _ _ _) <- packets+ ] of+ [selectedAEAD] ->+ assertEqual+ "AEAD capability negotiation should pick common preferred AEAD algorithm"+ GCM+ selectedAEAD+ other ->+ assertFailure ("Expected one SEIPDv2 packet, got " ++ show other)++testEncryptRecipientsNegotiationFailsWithoutCommonAEADAlgorithm+ :: Assertion+testEncryptRecipientsNegotiationFailsWithoutCommonAEADAlgorithm = 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.fromList [FeatureSEIPDv1, FeatureSEIPDv2]+ , recipientCapabilityPreferredSymmetricAlgorithms = [AES256]+ , recipientCapabilityPreferredAEADAlgorithms = [OCB]+ }+ v4Caps =+ RecipientCapabilities+ { recipientCapabilityKeyVersion = V4+ , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient+ , recipientCapabilityKeyFlags = Set.empty+ , recipientCapabilityFeatures =+ Set.fromList [FeatureSEIPDv1, FeatureSEIPDv2]+ , recipientCapabilityPreferredSymmetricAlgorithms = [AES256]+ , recipientCapabilityPreferredAEADAlgorithms = [GCM]+ }+ request =+ RecipientEncryptRequest+ { recipientEncryptRequestTargets =+ [ recipientEncryptionTargetWithCapabilities v6Recipient v6Caps+ , recipientEncryptionTargetWithCapabilities v4Recipient v4Caps+ ]+ , recipientEncryptRequestPayloadShape =+ defaultRecipientPayloadShape+ , recipientEncryptRequestPayload =+ "recipient AEAD mismatch payload"+ , recipientEncryptRequestSymmetricOverride = Just AES256+ , recipientEncryptRequestOverrides =+ RecipientEncryptRequestSEIPDv2Overrides+ { recipientEncryptRequestAEADOverride = Nothing+ , recipientEncryptRequestChunkSizeOverride = Just 6+ , recipientEncryptRequestSaltOverride =+ Just (Salt (B.replicate 32 0x27))+ }+ }+ result <-+ encryptForRecipientsWithCapabilityNegotiation+ RecipientCapabilityNegotiationOn+ request+ case result of+ Left+ ( RecipientCapabilitySelectionFailure+ (RecipientCapabilityNoCommonAEADAlgorithms _)+ ) -> pure ()+ Left other ->+ assertFailure+ ( "Expected RecipientCapabilityNoCommonAEADAlgorithms, got "+ ++ show other+ )+ Right _ ->+ assertFailure+ "Expected recipient capability negotiation to fail without common AEAD algorithm"++testEncryptRecipientsSEIPDv2FallsBackWhenRecipientsDoNotAdvertiseV2+ :: Assertion+testEncryptRecipientsSEIPDv2FallsBackWhenRecipientsDoNotAdvertiseV2 = do+ (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner+ let v4Recipient = setKeyVersion V4 baseRecipient+ v6Recipient = setKeyVersion V6 baseRecipient+ sharedCaps keyVersion recipient =+ RecipientCapabilities+ { recipientCapabilityKeyVersion = keyVersion+ , recipientCapabilityPublicKeyAlgorithm = _pkalgo recipient+ , recipientCapabilityKeyFlags = Set.empty+ , recipientCapabilityFeatures = Set.singleton FeatureSEIPDv1+ , recipientCapabilityPreferredSymmetricAlgorithms = [AES256]+ , recipientCapabilityPreferredAEADAlgorithms = [OCB]+ }+ request =+ RecipientEncryptRequest+ { recipientEncryptRequestTargets =+ [ recipientEncryptionTargetWithCapabilities+ v6Recipient+ (sharedCaps V6 v6Recipient)+ , recipientEncryptionTargetWithCapabilities+ v4Recipient+ (sharedCaps V4 v4Recipient)+ ]+ , recipientEncryptRequestPayloadShape =+ defaultRecipientPayloadShape+ , recipientEncryptRequestPayload =+ "recipient seipd fallback payload"+ , recipientEncryptRequestSymmetricOverride = Just AES256+ , recipientEncryptRequestOverrides =+ RecipientEncryptRequestSEIPDv2Overrides+ { recipientEncryptRequestAEADOverride = Nothing+ , recipientEncryptRequestChunkSizeOverride = Just 6+ , recipientEncryptRequestSaltOverride =+ Just (Salt (B.replicate 32 0x28))+ }+ }+ result <- encryptForRecipients request+ case result of+ Left err ->+ assertFailure+ ( "Expected fallback to SEIPDv1 when recipients do not advertise SEIPDv2, got "+ ++ show err+ )+ Right RecipientEncryptResult {recipientEncryptPackets = packets} -> do+ let seipdPkts = [p | SymEncIntegrityProtectedDataPkt p <- packets]+ case seipdPkts of+ [SEIPD1 1 _] -> pure ()+ [SEIPD2 {}] ->+ assertFailure+ "Expected SEIPDv1 fallback when recipients do not advertise SEIPDv2 support"+ other -> assertFailure ("Unexpected SEIPD packets: " ++ show other)++testEncryptRecipientsRejectsSEIPDv1WhenRecipientsDoNotAdvertiseMDC+ :: Assertion+testEncryptRecipientsRejectsSEIPDv1WhenRecipientsDoNotAdvertiseMDC = do+ (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner+ let v4Recipient = setKeyVersion V4 baseRecipient+ caps =+ RecipientCapabilities+ { recipientCapabilityKeyVersion = V4+ , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient+ , recipientCapabilityKeyFlags = Set.empty+ , recipientCapabilityFeatures = Set.singleton FeatureSEIPDv2+ , recipientCapabilityPreferredSymmetricAlgorithms = [AES256]+ , recipientCapabilityPreferredAEADAlgorithms = [OCB]+ }+ request =+ RecipientEncryptRequest+ { recipientEncryptRequestTargets =+ [recipientEncryptionTargetWithCapabilities v4Recipient caps]+ , recipientEncryptRequestPayloadShape =+ defaultRecipientPayloadShape+ , recipientEncryptRequestPayload =+ "recipient seipd v1 capability failure payload"+ , recipientEncryptRequestSymmetricOverride = Just AES256+ , recipientEncryptRequestOverrides =+ RecipientEncryptRequestSEIPDv1Overrides+ { recipientEncryptRequestIVOverride = Nothing+ }+ }+ result <- encryptForRecipients request+ case result of+ Left+ ( RecipientCapabilitySelectionFailure+ (RecipientCapabilityMissingSEIPDv1Support _)+ ) -> pure ()+ Left other ->+ assertFailure+ ( "Expected RecipientCapabilityMissingSEIPDv1Support, got "+ ++ show other+ )+ Right _ ->+ assertFailure+ "Expected SEIPDv1 encryption to fail when recipients do not advertise MDC support"++testRecipientEncryptionTargetsFromTKIncludesOnlyEncryptionCapableKeys+ :: Assertion+testRecipientEncryptionTargetsFromTKIncludesOnlyEncryptionCapableKeys = do+ (signingPrimary, _signingKey) <- loadDeterministicEd25519Signer+ (baseEncryptingSubkey, _privateKey) <- loadUnencryptedRsaSigner+ let tkUnknown =+ TKUnknown+ { _tkuKey = (signingPrimary, Nothing)+ , _tkuRevs = []+ , _tkuUIDs = []+ , _tkuUAts = []+ , _tkuSubs =+ [+ ( PublicSubkeyPkt+ (setKeyTimestamp (_timestamp signingPrimary) baseEncryptingSubkey)+ , []+ )+ ]+ }+ tk =+ case fromUnknownToTK tkUnknown of+ Right (SomePublicTK publicTk) -> publicTk+ Right (SomeSecretTK _) -> error "expected public TK for recipient target test"+ Left err -> error err+ encryptingSubkey =+ setKeyTimestamp (_timestamp signingPrimary) baseEncryptingSubkey+ targets = recipientEncryptionTargetsFromTK tk+ assertEqual+ "TKUnknown-derived targets should include only encryption-capable keys"+ 1+ (length targets)+ case targets of+ [target] ->+ assertEqual+ "TKUnknown-derived target should preserve selected encryption key"+ (fingerprint encryptingSubkey)+ (fingerprint (recipientEncryptionTargetKey target))+ _ ->+ assertFailure "Expected exactly one encryption-capable target"++testRecipientEncryptionTargetFromTKPrefersSubkeyOverPrimary+ :: Assertion+testRecipientEncryptionTargetFromTKPrefersSubkeyOverPrimary = do+ (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner+ let primary = setKeyVersion V4 baseRecipient+ subkey = setKeyVersion V6 baseRecipient+ tkUnknown =+ TKUnknown+ { _tkuKey = (primary, Nothing)+ , _tkuRevs = []+ , _tkuUIDs = []+ , _tkuUAts = []+ , _tkuSubs = [(PublicSubkeyPkt subkey, [])]+ }+ tk =+ case fromUnknownToTK tkUnknown of+ Right (SomePublicTK publicTk) -> publicTk+ Right (SomeSecretTK _) ->+ error "expected public TK for recipient target selection test"+ Left err -> error err+ case recipientEncryptionTargetFromTK tk of+ Right target ->+ assertEqual+ "TKUnknown-derived single target should prioritize encryption subkeys"+ (fingerprint subkey)+ (fingerprint (recipientEncryptionTargetKey target))+ Left err ->+ assertFailure+ ( "Expected TKUnknown-derived target selection to succeed, got "+ ++ show err+ )++testRecipientEncryptionTargetFromTKRejectsTKWithoutEncryptableKeys+ :: Assertion+testRecipientEncryptionTargetFromTKRejectsTKWithoutEncryptableKeys = do+ (signingPrimary, _signingKey) <- loadDeterministicEd25519Signer+ let tkUnknown =+ TKUnknown+ { _tkuKey = (signingPrimary, Nothing)+ , _tkuRevs = []+ , _tkuUIDs = []+ , _tkuUAts = []+ , _tkuSubs = []+ }+ tk =+ case fromUnknownToTK tkUnknown of+ Right (SomePublicTK publicTk) -> publicTk+ Right (SomeSecretTK _) -> error "expected public TK for recipient rejection test"+ Left err -> error err+ case recipientEncryptionTargetFromTK tk of+ Left RecipientCapabilityNoEncryptableKeyMaterialInTK -> pure ()+ Left other ->+ assertFailure+ ( "Expected RecipientCapabilityNoEncryptableKeyMaterialInTK, got "+ ++ show other+ )+ Right _ ->+ assertFailure+ "Expected TKUnknown-derived target selection to reject non-encryptable TKs"++testRecipientEncryptionTargetsReportFromTKAtTimestampExplainsRejections+ :: Assertion+testRecipientEncryptionTargetsReportFromTKAtTimestampExplainsRejections = do+ (baseKey, signingKey) <- loadUnencryptedRsaSigner+ let signatureTime = ThirtyTwoBitTimeStamp 1700000000+ primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey+ subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000001) baseKey+ subkeyBindingSig <-+ signSubkeyBindingWithRSAExtrasAt+ primary+ subkey+ signingKey+ signatureTime+ [SigSubPacket False (KeyFlags (Set.fromList [SignDataKey]))]+ let tk =+ TKUnknown+ { _tkuKey = (primary, Nothing)+ , _tkuRevs = []+ , _tkuUIDs = []+ , _tkuUAts = []+ , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]+ }+ report =+ recipientEncryptionTargetsReportFromTKAtTimestamp+ signatureTime+ tk+ acceptedFps =+ map+ (fingerprint . recipientEncryptionTargetKey)+ (recipientEncryptionTargetsAccepted report)+ rejectedReasons =+ map+ recipientEncryptionTargetRejectedReason+ (recipientEncryptionTargetsRejected report)+ assertBool+ "report should keep encryption-eligible primary key as accepted"+ (fingerprint primary `elem` acceptedFps)+ assertEqual+ "report should explain sign-only subkey rejection"+ [ RecipientTargetMissingEncryptionFlags+ subkey+ (Set.fromList [SignDataKey])+ ]+ rejectedReasons++testRecipientEncryptionTargetsReportFromTKAtTimestampExplainsUnsupportedAlgorithms+ :: Assertion+testRecipientEncryptionTargetsReportFromTKAtTimestampExplainsUnsupportedAlgorithms = do+ (signingPrimary, _signingKey) <- loadDeterministicEd25519Signer+ let tk =+ TKUnknown+ { _tkuKey = (signingPrimary, Nothing)+ , _tkuRevs = []+ , _tkuUIDs = []+ , _tkuUAts = []+ , _tkuSubs = []+ }+ report =+ recipientEncryptionTargetsReportFromTKAtTimestamp+ (_timestamp signingPrimary)+ tk+ assertEqual+ "unsupported key algorithms should be surfaced in report rejections"+ [RecipientTargetUnsupportedAlgorithm (_pkalgo signingPrimary)]+ ( map+ recipientEncryptionTargetRejectedReason+ (recipientEncryptionTargetsRejected report)+ )++testRecipientEncryptionTargetsReportFromTKAtTimestampRejectsExpiredSubkey+ :: Assertion+testRecipientEncryptionTargetsReportFromTKAtTimestampRejectsExpiredSubkey = do+ (baseKey, signingKey) <- loadUnencryptedRsaSigner+ let signatureTime = ThirtyTwoBitTimeStamp 1700000000+ targetTime = ThirtyTwoBitTimeStamp 1700000020+ primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey+ subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000001) baseKey+ subkeyBindingSig <-+ signSubkeyBindingWithRSAExtrasAt+ primary+ subkey+ signingKey+ signatureTime+ [ SigSubPacket+ False+ (KeyFlags (Set.fromList [EncryptCommunicationsKey]))+ , SigSubPacket False (KeyExpirationTime (ThirtyTwoBitDuration 5))+ ]+ let tk =+ TKUnknown+ { _tkuKey = (primary, Nothing)+ , _tkuRevs = []+ , _tkuUIDs = []+ , _tkuUAts = []+ , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]+ }+ report =+ recipientEncryptionTargetsReportFromTKAtTimestamp targetTime tk+ rejectedReasons =+ map+ recipientEncryptionTargetRejectedReason+ (recipientEncryptionTargetsRejected report)+ assertBool+ "expired subkey should be rejected from recipient targets"+ ( RecipientTargetNotValidAtTimestamp subkey targetTime+ `elem` rejectedReasons+ )++testRecipientEncryptionTargetsReportFromTKAtTimestampRejectsRevokedSubkey+ :: Assertion+testRecipientEncryptionTargetsReportFromTKAtTimestampRejectsRevokedSubkey = do+ (baseKey, signingKey) <- loadUnencryptedRsaSigner+ let bindingTime = ThirtyTwoBitTimeStamp 1700000000+ revocationTime = ThirtyTwoBitTimeStamp 1700000005+ targetTime = ThirtyTwoBitTimeStamp 1700000010+ primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey+ subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000001) baseKey+ subkeyBindingSig <-+ signSubkeyBindingWithRSAExtrasAt+ primary+ subkey+ signingKey+ bindingTime+ [ SigSubPacket+ False+ (KeyFlags (Set.fromList [EncryptCommunicationsKey]))+ ]+ subkeyRevocationSig <-+ signSubkeyRevocationWithRSAAt+ primary+ subkey+ signingKey+ revocationTime+ let tk =+ TKUnknown+ { _tkuKey = (primary, Nothing)+ , _tkuRevs = []+ , _tkuUIDs = []+ , _tkuUAts = []+ , _tkuSubs =+ [ (PublicSubkeyPkt subkey, [subkeyBindingSig, subkeyRevocationSig])+ ]+ }+ report =+ recipientEncryptionTargetsReportFromTKAtTimestamp targetTime tk+ rejectedReasons =+ map+ recipientEncryptionTargetRejectedReason+ (recipientEncryptionTargetsRejected report)+ assertBool+ "revoked subkey should be rejected from recipient targets"+ (RecipientTargetRevoked subkey `elem` rejectedReasons)++testRecipientEncryptionTargetsFromTKAtTimestampExtractsSelfSigCapabilities+ :: Assertion+testRecipientEncryptionTargetsFromTKAtTimestampExtractsSelfSigCapabilities = do+ (primary, signingKey) <- loadUnencryptedRsaSigner+ (subkey, _privateKey) <- loadUnencryptedRsaSigner+ let signatureTime = ThirtyTwoBitTimeStamp 1700000000+ directKeySig <-+ signDirectKeyWithRSAExtrasAt+ primary+ signingKey+ signatureTime+ [ SigSubPacket False (PreferredSymmetricAlgorithms [AES128])+ , SigSubPacket False (OtherSigSub 39 (BL.pack [9, 2, 7, 3]))+ ]+ subkeyBindingSig <-+ signSubkeyBindingWithRSAExtrasAt+ primary+ subkey+ signingKey+ signatureTime+ [ SigSubPacket+ False+ (KeyFlags (Set.fromList [EncryptCommunicationsKey]))+ ]+ let tk =+ TKUnknown+ { _tkuKey = (primary, Nothing)+ , _tkuRevs = [directKeySig]+ , _tkuUIDs = []+ , _tkuUAts = []+ , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]+ }+ targets = recipientEncryptionTargetsFromTKAtTimestamp signatureTime tk+ case targets of+ (target : _) ->+ case recipientEncryptionTargetCapabilities target of+ Nothing ->+ assertFailure+ "Expected TKUnknown-derived target to include extracted capabilities"+ Just caps -> do+ assertEqual+ "self-signature capability extraction should include primary-key symmetric preferences"+ [AES128]+ (recipientCapabilityPreferredSymmetricAlgorithms caps)+ assertEqual+ "self-signature capability extraction should include primary-key preferred AEAD algorithms"+ [OCB, GCM]+ (recipientCapabilityPreferredAEADAlgorithms caps)+ assertEqual+ "self-signature capability extraction should include subkey key flags"+ (Set.fromList [EncryptCommunicationsKey])+ (recipientCapabilityKeyFlags caps)+ [] ->+ assertFailure "Expected at least one TKUnknown-derived target"++testRecipientEncryptionTargetFromTKAtTimestampAppliesTimestampFiltering+ :: Assertion+testRecipientEncryptionTargetFromTKAtTimestampAppliesTimestampFiltering = do+ (primary, signingKey) <- loadUnencryptedRsaSigner+ (subkey, _privateKey) <- loadUnencryptedRsaSigner+ let signatureTime = ThirtyTwoBitTimeStamp 1700000100+ beforeSignature = ThirtyTwoBitTimeStamp 1699999999+ subkeyBindingSig <-+ signSubkeyBindingWithRSAExtrasAt+ primary+ subkey+ signingKey+ signatureTime+ [ SigSubPacket+ False+ (KeyFlags (Set.fromList [EncryptCommunicationsKey]))+ ]+ let tk =+ TKUnknown+ { _tkuKey = (primary, Nothing)+ , _tkuRevs = []+ , _tkuUIDs = []+ , _tkuUAts = []+ , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]+ }+ case recipientEncryptionTargetFromTKAtTimestamp beforeSignature tk of+ Left err ->+ assertFailure+ ( "Expected timestamp-scoped TKUnknown 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"+ Just caps ->+ assertEqual+ "subkey binding created after target timestamp should not contribute key flags"+ Set.empty+ (recipientCapabilityKeyFlags caps)++testRecipientEncryptionTargetFromTKAtTimestampWithPolicyPrefersPrimary+ :: Assertion+testRecipientEncryptionTargetFromTKAtTimestampWithPolicyPrefersPrimary = do+ (baseKey, signingKey) <- loadUnencryptedRsaSigner+ let signatureTime = ThirtyTwoBitTimeStamp 1700000000+ primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey+ subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000001) baseKey+ subkeyBindingSig <-+ signSubkeyBindingWithRSAExtrasAt+ primary+ subkey+ signingKey+ signatureTime+ [ SigSubPacket+ False+ (KeyFlags (Set.fromList [EncryptCommunicationsKey]))+ ]+ let tk =+ TKUnknown+ { _tkuKey = (primary, Nothing)+ , _tkuRevs = []+ , _tkuUIDs = []+ , _tkuUAts = []+ , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]+ }+ case recipientEncryptionTargetFromTKAtTimestampWithPolicy+ RecipientTargetSelectionPreferPrimary+ signatureTime+ tk of+ Left err ->+ assertFailure+ ( "Expected policy-based recipient selection to succeed, got "+ ++ show err+ )+ Right target ->+ assertEqual+ "prefer-primary policy should select primary key when both primary and subkey are valid"+ (fingerprint primary)+ (fingerprint (recipientEncryptionTargetKey target))++testRecipientEncryptionTargetFromTKAtTimestampWithPolicyPrefersNewest+ :: Assertion+testRecipientEncryptionTargetFromTKAtTimestampWithPolicyPrefersNewest = do+ (baseKey, signingKey) <- loadUnencryptedRsaSigner+ let signatureTime = ThirtyTwoBitTimeStamp 1700000000+ primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey+ subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000002) baseKey+ subkeyBindingSig <-+ signSubkeyBindingWithRSAExtrasAt+ primary+ subkey+ signingKey+ signatureTime+ [ SigSubPacket+ False+ (KeyFlags (Set.fromList [EncryptCommunicationsKey]))+ ]+ let tk =+ TKUnknown+ { _tkuKey = (primary, Nothing)+ , _tkuRevs = []+ , _tkuUIDs = []+ , _tkuUAts = []+ , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]+ }+ case recipientEncryptionTargetFromTKAtTimestampWithPolicy+ RecipientTargetSelectionPreferNewestCreationTime+ signatureTime+ tk of+ Left err ->+ assertFailure+ ( "Expected policy-based recipient selection to succeed, got "+ ++ show err+ )+ Right target ->+ assertEqual+ "prefer-newest policy should select newest valid key"+ (fingerprint subkey)+ (fingerprint (recipientEncryptionTargetKey target))++testRecipientEncryptionTargetsFromTKRejectsKeyWithNonEncryptFlags+ :: Assertion+testRecipientEncryptionTargetsFromTKRejectsKeyWithNonEncryptFlags = do+ (baseKey, signingKey) <- loadUnencryptedRsaSigner+ let signatureTime = ThirtyTwoBitTimeStamp 1700000000+ primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey+ subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000001) baseKey+ subkeyBindingSig <-+ signSubkeyBindingWithRSAExtrasAt+ primary+ subkey+ signingKey+ signatureTime+ [SigSubPacket False (KeyFlags (Set.fromList [SignDataKey]))]+ let tk =+ TKUnknown+ { _tkuKey = (primary, Nothing)+ , _tkuRevs = []+ , _tkuUIDs = []+ , _tkuUAts = []+ , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]+ }+ targets = recipientEncryptionTargetsFromTKAtTimestamp signatureTime tk+ let subkeyFp = fingerprint subkey+ primaryFp = fingerprint primary+ targetFps = map (fingerprint . recipientEncryptionTargetKey) targets+ assertBool+ "subkey with sign-only key flags should be excluded from encryption targets"+ (subkeyFp `notElem` targetFps)+ assertBool+ "primary key (no explicit flags) should still be included as encryption target"+ (primaryFp `elem` targetFps)++testRecipientEncryptionTargetsFromTKIncludesKeyWithNoFlags+ :: Assertion+testRecipientEncryptionTargetsFromTKIncludesKeyWithNoFlags = do+ (baseKey, signingKey) <- loadUnencryptedRsaSigner+ let signatureTime = ThirtyTwoBitTimeStamp 1700000000+ primary = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000000) baseKey+ subkey = setKeyTimestamp (ThirtyTwoBitTimeStamp 1600000001) baseKey+ subkeyBindingSig <-+ signSubkeyBindingWithRSAExtrasAt+ primary+ subkey+ signingKey+ signatureTime+ []+ let tk =+ TKUnknown+ { _tkuKey = (primary, Nothing)+ , _tkuRevs = []+ , _tkuUIDs = []+ , _tkuUAts = []+ , _tkuSubs = [(PublicSubkeyPkt subkey, [subkeyBindingSig])]+ }+ targets = recipientEncryptionTargetsFromTKAtTimestamp signatureTime tk+ let subkeyFp = fingerprint subkey+ targetFps = map (fingerprint . recipientEncryptionTargetKey) targets+ assertBool+ "subkey with no key flags should be included as encryption target (legacy-compatible)"+ (subkeyFp `elem` targetFps)++testEncryptRecipientsWithRequestEmitsOnePassSignatures+ :: Assertion+testEncryptRecipientsWithRequestEmitsOnePassSignatures = do+ (baseRecipient, privateKey) <- loadUnencryptedRsaSigner+ let recipient = setKeyVersion V6 baseRecipient+ signature =+ SigV4+ BinarySig+ RSA+ SHA256+ []+ [ SigSubPacket+ False+ (Issuer (EightOctetKeyId (BL.pack [0x01 .. 0x08])))+ ]+ 0+ (MPI 1 :| [])+ payload = "recipient one-pass signature payload"+ request =+ RecipientEncryptRequest+ { recipientEncryptRequestTargets =+ [recipientEncryptionTarget recipient]+ , recipientEncryptRequestPayloadShape =+ defaultRecipientPayloadShape+ { recipientPayloadUseOnePassSignatures = True+ , recipientPayloadSignatures = [signature]+ }+ , recipientEncryptRequestPayload = payload+ , recipientEncryptRequestSymmetricOverride = Just AES256+ , recipientEncryptRequestOverrides =+ RecipientEncryptRequestSEIPDv2Overrides+ { recipientEncryptRequestAEADOverride = Just OCB+ , recipientEncryptRequestChunkSizeOverride = Just 6+ , recipientEncryptRequestSaltOverride =+ Just (Salt (B.replicate 32 0x55))+ }+ }+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipient+ , pkeskRecipientSKey = RSAPrivateKey (RSA_PrivateKey privateKey)+ }+ )+ )+ passphraseCallback _ = pure BL.empty+ result <- encryptForRecipients request+ packets <-+ case result of+ Left err ->+ assertFailure+ ( "Expected one-pass encryption request to succeed, got "+ ++ show err+ )+ >> fail "encryptForRecipients failed"+ Right+ RecipientEncryptResult+ { recipientEncryptPackets = encryptedPackets+ } -> pure encryptedPackets+ decrypted <-+ DC.runConduitRes $+ CL.sourceList packets+ DC..| conduitDecryptWithPKESKContext+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [ OnePassSignaturePkt+ (OPSPayloadV3Packet (OPSPayloadV3 3 BinarySig SHA256 RSA _ False))+ , LiteralDataPkt _ _ _ gotPayload+ , SignaturePkt _+ ] ->+ assertEqual+ "decrypted payload matches original plaintext"+ (BL.fromStrict payload)+ gotPayload+ other ->+ assertFailure+ ( "Expected decrypted [OPS3, LiteralData, Signature], got "+ ++ show other+ )++testEncryptRecipientsWithRequestRejectsOnePassWhenIssuerMetadataMissing+ :: Assertion+testEncryptRecipientsWithRequestRejectsOnePassWhenIssuerMetadataMissing = do+ (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner+ let recipient = setKeyVersion V6 baseRecipient+ signature = SigV4 BinarySig RSA SHA256 [] [] 0 (MPI 1 :| [])+ request =+ RecipientEncryptRequest+ { recipientEncryptRequestTargets =+ [recipientEncryptionTarget recipient]+ , recipientEncryptRequestPayloadShape =+ defaultRecipientPayloadShape+ { recipientPayloadUseOnePassSignatures = True+ , recipientPayloadSignatures = [signature]+ }+ , recipientEncryptRequestPayload =+ "recipient one-pass missing issuer metadata payload"+ , recipientEncryptRequestSymmetricOverride = Just AES256+ , recipientEncryptRequestOverrides =+ RecipientEncryptRequestSEIPDv2Overrides+ { recipientEncryptRequestAEADOverride = Just OCB+ , recipientEncryptRequestChunkSizeOverride = Just 6+ , recipientEncryptRequestSaltOverride =+ Just (Salt (B.replicate 32 0x56))+ }+ }+ result <- encryptForRecipients request+ case result of+ Left (PayloadBuildFailure err) ->+ assertBool+ "missing issuer metadata should surface as payload build failure"+ ("without issuer metadata" `isInfixOf` err)+ Left err ->+ assertFailure+ ( "Expected PayloadBuildFailure for missing OPS issuer metadata, got "+ ++ show err+ )+ Right _ ->+ assertFailure+ "Expected one-pass request without issuer metadata to fail"++testEncryptInteropLegacyProducesSEIPDv1 :: Assertion+testEncryptInteropLegacyProducesSEIPDv1 = do+ (recipient, privateKey) <- loadUnencryptedRsaSigner+ let iv = IV (B.replicate 16 0xAB)+ payload = "legacy interop payload"+ request =+ RecipientEncryptRequest+ { recipientEncryptRequestTargets =+ [recipientEncryptionTarget recipient]+ , recipientEncryptRequestPayloadShape =+ defaultRecipientPayloadShape+ , recipientEncryptRequestPayload = payload+ , recipientEncryptRequestSymmetricOverride = Just AES256+ , recipientEncryptRequestOverrides =+ RecipientEncryptRequestSEIPDv1Overrides+ { recipientEncryptRequestIVOverride = Just iv+ }+ }+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipient+ , pkeskRecipientSKey = RSAPrivateKey (RSA_PrivateKey privateKey)+ }+ )+ )+ passphraseCallback _ = pure BL.empty+ result <- encryptForRecipients request+ packets <-+ case result of+ Left err ->+ assertFailure+ ( "Expected EncryptInteropLegacy request to succeed, got "+ ++ show err+ )+ >> fail "encryptForRecipients failed"+ Right RecipientEncryptResult {recipientEncryptPackets = ps} -> pure ps+ let seipdPkts = [p | SymEncIntegrityProtectedDataPkt p <- packets]+ case seipdPkts of+ [SEIPD1 1 _] -> pure ()+ [SEIPD2 {}] -> assertFailure "Expected SEIPDv1 but got SEIPDv2"+ other -> assertFailure ("Unexpected SEIPD packets: " ++ show other)+ let pkeskPkts = [p | PKESKPkt p <- packets]+ assertBool "EncryptInteropLegacy should emit PKESKv3 packets" $+ all+ (\p -> case p of PKESKPayloadV3Packet {} -> True; _ -> False)+ pkeskPkts+ decrypted <-+ DC.runConduitRes $+ CL.sourceList packets+ DC..| conduitDecryptWithPKESKContext+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "decrypted legacy interop payload matches original"+ (BL.fromStrict payload)+ gotPayload+ other ->+ assertFailure+ ("Expected [LiteralData] from legacy decrypt, got " ++ show other)++testCanonicalizePKESKRecipientIdHelper :: Assertion+testCanonicalizePKESKRecipientIdHelper = do+ let rid = BL.pack (0x04 : replicate 20 0x11)+ payload = PKESKPayloadV6Packet (PKESKPayloadV6 rid RSA "esk")+ case canonicalizePKESKRecipientId payload of+ Right (PKESKPayloadV6Packet (PKESKPayloadV6 normalized _ _))+ | BL.length normalized == 20 -> pure ()+ | otherwise ->+ assertFailure+ ( "Expected canonicalized recipient id length 20, got "+ ++ show (BL.length normalized)+ )+ Left err ->+ assertFailure+ ("canonicalizePKESKRecipientId failed unexpectedly: " ++ show err)+ Right other ->+ assertFailure+ ( "Expected PKESK6 payload after canonicalization, got "+ ++ show other+ )+ case canonicalizePKESKRecipientId+ ( PKESKPayloadV6Packet+ (PKESKPayloadV6 (BL.replicate 19 0x22) RSA "esk")+ ) of+ Left (InvalidRecipientIdentifier _) -> pure ()+ other ->+ assertFailure+ ( "Expected InvalidRecipientIdentifier for malformed rid, got "+ ++ show other+ )+ case canonicalizePKESKRecipientId+ ( PKESKPayloadV6Packet+ (PKESKPayloadV6 (BL.pack (0x06 : replicate 32 0x33)) RSA "esk")+ ) of+ Right (PKESKPayloadV6Packet (PKESKPayloadV6 normalized _ _))+ | BL.length normalized == 32 -> pure ()+ | otherwise ->+ assertFailure+ ( "Expected canonicalized v6 recipient id length 32, got "+ ++ show (BL.length normalized)+ )+ other ->+ assertFailure+ ( "Expected 0x06-prefixed v6 recipient id to canonicalize, got "+ ++ show other+ )+ case canonicalizePKESKRecipientId+ ( PKESKPayloadV6Packet+ (PKESKPayloadV6 (BL.pack (0x04 : replicate 32 0x44)) RSA "esk")+ ) of+ Left (InvalidRecipientIdentifier _) -> pure ()+ other ->+ assertFailure+ ( "Expected InvalidRecipientIdentifier for mismatched v4-prefixed v6-length rid, got "+ ++ show other+ )++testBuildPKESKPayloadUnsupportedRecipient :: Assertion+testBuildPKESKPayloadUnsupportedRecipient = do+ let recipient =+ PKPayload+ V6+ (ThirtyTwoBitTimeStamp 0)+ 0+ EdDSA+ (EdDSAPubKey EdSigningCurve25519 (NativeEPoint (EPoint 1)))+ sessionKey = SessionKey (B.replicate 32 0x19)+ sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey+ result <-+ buildPKESKPayloadForRecipient PreferV6 recipient sessionMaterial+ case result of+ Left (UnsupportedRecipientAlgorithm EdDSA) -> pure ()+ Left err ->+ assertFailure+ ("Expected UnsupportedRecipientAlgorithm EdDSA, got " ++ show err)+ Right payload ->+ assertFailure+ ( "Expected UnsupportedRecipientAlgorithm, got payload: "+ ++ show payload+ )++testBuildPKESKPayloadRejectsECDHSHA1 :: Assertion+testBuildPKESKPayloadRejectsECDHSHA1 = do+ let (recipientPub, _) = syntheticNISTP256Low+ recipient =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ ECDH+ ( ECDHPubKey+ (ECDSAPubKey (ECDSA_PublicKey recipientPub))+ SHA1+ AES256+ )+ sessionKey = SessionKey (B.replicate 32 0x19)+ sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey+ 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 err ->+ assertFailure+ ("Expected RecipientKdfFailure ECDH for SHA1, got " ++ show err)+ Right payload ->+ assertFailure+ ("Expected SHA1 policy rejection, got payload: " ++ show payload)++testConduitDecryptSEIPDv2FixtureWithMatchingV6SecretKey+ :: Assertion+testConduitDecryptSEIPDv2FixtureWithMatchingV6SecretKey = do+ armoredMessage <- readFixtureStrict "seipdv2.pgp.aa"+ armoredEncryptedSecret <-+ readFixtureStrict "v6-encrypted-secret.pgp.aa"+ armoredPlainSecret <- readFixtureStrict "v6-secret.pgp.aa"+ passphrase <- readPKIPassphrase+ messageArmors <-+ case (AA.decode armoredMessage :: Either String [Armor]) of+ Left err ->+ assertFailure ("failed to decode seipdv2 armor: " ++ err)+ >> pure []+ Right as -> pure as+ encryptedSecretArmors <-+ case (AA.decode armoredEncryptedSecret :: Either String [Armor]) of+ Left err ->+ assertFailure+ ("failed to decode v6 encrypted secret armor: " ++ err)+ >> pure []+ Right as -> pure as+ plainSecretArmors <-+ case (AA.decode armoredPlainSecret :: Either String [Armor]) of+ Left err ->+ assertFailure ("failed to decode v6 plain secret armor: " ++ err)+ >> pure []+ Right as -> pure as+ messageBody <-+ case messageArmors of+ (a : _) -> pure (armorPayload a)+ [] ->+ assertFailure "seipdv2 armor file contained no armor blocks"+ >> pure mempty+ encryptedSecretBody <-+ case encryptedSecretArmors of+ (a : _) -> pure (armorPayload a)+ [] ->+ assertFailure+ "v6-encrypted-secret armor file contained no armor blocks"+ >> pure mempty+ plainSecretBody <-+ case plainSecretArmors of+ (a : _) -> pure (armorPayload a)+ [] ->+ assertFailure "v6-secret armor file contained no armor blocks"+ >> pure mempty+ let messagePackets = parsePkts messageBody+ encryptedSecretPackets = parsePkts encryptedSecretBody+ plainSecretPackets = parsePkts plainSecretBody+ passphraseCallback _ = pure BL.empty+ allKeyInfos <-+ collectSecretKeyInfos+ (encryptedSecretPackets ++ plainSecretPackets)+ passphrase+ let keyContextCallback pkt = pure (selectRecipientKeyInfo pkt allKeyInfos)+ if null allKeyInfos+ then+ assertFailure+ ( "no usable secret key material found in v6-encrypted-secret fixture: "+ ++ show+ ( [ (_pkalgo pkp, ska)+ | SecretKeyPkt pkp ska <- encryptedSecretPackets+ ]+ ++ [ (_pkalgo pkp, ska)+ | SecretSubkeyPkt pkp ska <- encryptedSecretPackets+ ]+ )+ )+ else do+ decrypted <-+ DC.runConduitRes $+ CL.sourceList messagePackets+ DC..| conduitDecryptWithPKESKContext+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case [p | LiteralDataPkt _ _ _ p <- decrypted] of+ (payload : _) ->+ if BL.null payload+ then+ assertFailure "fixture decrypt produced empty literal payload"+ else pure ()+ [] ->+ assertFailure+ ( "expected decrypted literal payload from seipdv2 fixture, got: "+ ++ show decrypted+ )++testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKey+ :: Assertion+testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKey = do+ (messagePackets, encryptedSecretPackets, passphrase) <-+ loadSEIPDv2FixtureWithV4Secret "seipdv2-for-v4-key.pgp.aa"+ let+ passphraseCallback _ = pure BL.empty+ keyInfos <-+ collectSecretKeyInfos encryptedSecretPackets passphrase+ let keyContextCallback pkt = pure (selectRecipientKeyInfo pkt keyInfos)+ decrypted <-+ DC.runConduitRes $+ CL.sourceList messagePackets+ DC..| conduitDecryptWithPKESKContext+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case [p | LiteralDataPkt _ _ _ p <- decrypted] of+ (payload : _) ->+ if "This is a test of SEIPDv1" `isInfixOf` BLC8.unpack payload+ then pure ()+ else+ assertFailure+ ( "unexpected decrypted payload for seipdv2-for-v4-key fixture: "+ ++ show payload+ )+ [] ->+ assertFailure+ ( "expected decrypted literal payload from seipdv2-for-v4-key fixture, got: "+ ++ show decrypted+ )++testConduitDecryptSEIPDv2FixtureIgnoresUnusableLatestPKESK+ :: Assertion+testConduitDecryptSEIPDv2FixtureIgnoresUnusableLatestPKESK = do+ (messagePackets, encryptedSecretPackets, passphrase) <-+ loadSEIPDv2FixtureWithV4Secret "seipdv2-for-v4-key.pgp.aa"+ let+ bogusRid = BL.pack (0x06 : replicate 32 0x99)+ bogusPKESK =+ PKESKPkt+ (PKESKPayloadV6Packet (PKESKPayloadV6 bogusRid RSA "bogus-esk"))+ (eskPrefix, encryptedSuffix) = span isPrecedingESK messagePackets+ packetsWithBogusLatestPKESK = eskPrefix ++ [bogusPKESK] ++ encryptedSuffix+ passphraseCallback _ = pure BL.empty+ keyInfos <-+ collectSecretKeyInfos encryptedSecretPackets passphrase+ let keyContextCallback pkt = pure (selectRecipientKeyInfo pkt keyInfos)+ decrypted <-+ DC.runConduitRes $+ CL.sourceList packetsWithBogusLatestPKESK+ DC..| conduitDecryptWithPKESKContext+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case [p | LiteralDataPkt _ _ _ p <- decrypted] of+ (payload : _) ->+ if "This is a test of SEIPDv1" `isInfixOf` BLC8.unpack payload+ then pure ()+ else+ assertFailure+ ( "unexpected decrypted payload for seipdv2-for-v4-key fixture with bogus latest PKESK: "+ ++ show payload+ )+ [] ->+ assertFailure+ ( "expected decrypted literal payload with bogus latest PKESK, got: "+ ++ show decrypted+ )++testConduitDecryptSEIPDv2FixtureAcceptsRecipientIdWithoutCallerPermutations+ :: Assertion+testConduitDecryptSEIPDv2FixtureAcceptsRecipientIdWithoutCallerPermutations = do+ (messagePacketsRaw, encryptedSecretPackets, passphrase) <-+ loadSEIPDv2FixtureWithV4Secret "seipdv2-for-v4-key.pgp.aa"+ let messagePackets = map forceVersionedRecipientIdentifier messagePacketsRaw+ passphraseCallback _ = pure BL.empty+ keyInfos <-+ collectSecretKeyInfos encryptedSecretPackets passphrase+ let keyContextCallback pkt = pure (selectRecipientKeyInfoByRawRecipientId pkt keyInfos)+ decrypted <-+ DC.runConduitRes $+ CL.sourceList messagePackets+ DC..| conduitDecryptWithPKESKContext+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case [p | LiteralDataPkt _ _ _ p <- decrypted] of+ (payload : _) ->+ if "This is a test of SEIPDv1" `isInfixOf` BLC8.unpack payload+ then pure ()+ else+ assertFailure+ ( "unexpected decrypted payload for recipient-id normalization fixture: "+ ++ show payload+ )+ [] ->+ assertFailure+ ( "expected decrypted literal payload for recipient-id normalization fixture, got: "+ ++ show decrypted+ )++testConduitDecryptPKESKFailureDoesNotRequireManualSessionMaterial+ :: Assertion+testConduitDecryptPKESKFailureDoesNotRequireManualSessionMaterial = do+ armors <- loadArmor "seipdv2-for-v4-key.pgp.aa"+ armor <-+ case armors of+ (a : _) -> pure a+ [] ->+ assertFailure+ "seipdv2-for-v4-key.pgp.aa should contain one armored payload"+ >> fail "expected one armored payload"+ let packets = parsePkts (armorPayload armor)+ keyContextCallback _ = pure Nothing+ passphraseCallback prompt =+ fail ("unexpected manual PKESK session key prompt: " ++ prompt)+ result <-+ catch+ ( Right+ <$> ( DC.runConduitRes $+ CL.sourceList packets+ DC..| conduitDecryptWithPKESKContext+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ )+ )+ (\e -> pure (Left (show (e :: SomeException))))+ case result of+ Left err -> do+ assertBool+ "failure should report PKESK candidate exhaustion"+ ("no matching key context" `isInfixOf` err)+ assertBool+ "failure should not request manual PKESK session material"+ ( not+ ("unexpected manual PKESK session key prompt" `isInfixOf` err)+ )+ Right decrypted ->+ assertFailure+ ( "expected decryption failure when no PKESK key context is available, got: "+ ++ show decrypted+ )++testConduitDecryptSEIPDv2TwoRecipientsFixtureWithMatchingV4SecretKey+ :: Assertion+testConduitDecryptSEIPDv2TwoRecipientsFixtureWithMatchingV4SecretKey =+ testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform+ "seipdv2-two-recipients.pgp.aa"+ id++testConduitDecryptSEIPDv2ThreeRecipientsFixtureWithMatchingV4SecretKey+ :: Assertion+testConduitDecryptSEIPDv2ThreeRecipientsFixtureWithMatchingV4SecretKey =+ testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform+ "seipdv2-three-recipients.pgp.aa"+ id++testConduitDecryptSEIPDv2TwoRecipientsFixtureWithReorderedAndUnusablePKESKs+ :: Assertion+testConduitDecryptSEIPDv2TwoRecipientsFixtureWithReorderedAndUnusablePKESKs =+ testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform+ "seipdv2-two-recipients.pgp.aa"+ (prependUnusableLatestPKESK . reorderPrecedingPKESKs)++testConduitDecryptSEIPDv2ThreeRecipientsFixtureWithReorderedAndUnusablePKESKs+ :: Assertion+testConduitDecryptSEIPDv2ThreeRecipientsFixtureWithReorderedAndUnusablePKESKs =+ testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform+ "seipdv2-three-recipients.pgp.aa"+ (prependUnusableLatestPKESK . reorderPrecedingPKESKs)++testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFile+ :: FilePath -> Assertion+testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFile fixture =+ testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform+ fixture+ id++testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform+ :: FilePath+ -> ([Pkt] -> [Pkt])+ -> Assertion+testConduitDecryptSEIPDv2FixtureWithMatchingV4SecretKeyFromFileWithTransform fixture transformPackets = do+ (messagePacketsRaw, encryptedSecretPackets, passphrase) <-+ loadSEIPDv2FixtureWithV4Secret fixture+ let messagePackets = transformPackets messagePacketsRaw+ passphraseCallback _ = pure BL.empty+ keyInfos <-+ collectSecretKeyInfos encryptedSecretPackets passphrase+ let keyContextCallback pkt = pure (selectRecipientKeyInfo pkt keyInfos)+ decrypted <-+ DC.runConduitRes $+ CL.sourceList messagePackets+ DC..| conduitDecryptWithPKESKContext+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case [p | LiteralDataPkt _ _ _ p <- decrypted] of+ (payload : _) ->+ if BL.null payload+ then+ assertFailure+ (fixture ++ " decrypt produced empty literal payload")+ else pure ()+ [] ->+ assertFailure+ ( "expected decrypted literal payload from "+ ++ fixture+ ++ ", got: "+ ++ show decrypted+ )++testConduitDecryptSEIPDv2WithPKESKv6RawSessionKey :: Assertion+testConduitDecryptSEIPDv2WithPKESKv6RawSessionKey = do+ let sessionKey = B.replicate 32 0x2a+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "v6 pkesk session key decrypt path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ packets ct =+ [ PKESKPkt+ ( PKESKPayloadV6Packet+ ( PKESKPayloadV6+ "\x01\x02\x03\x04\x05\x06\x07\x08"+ RSA+ "\x99\x88\x77"+ )+ )+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ct))+ ]+ callback _ = pure (BL.fromStrict sessionKey)+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ (SessionKey sessionKey)+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList (packets ciphertext)+ DC..| conduitDecrypt callback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "PKESKv6 raw session-key decrypt payload"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptSEIPDv2RejectsWrongPKESKv6RawSessionKeyLength+ :: Assertion+testConduitDecryptSEIPDv2RejectsWrongPKESKv6RawSessionKeyLength = do+ let sessionKey = B.replicate 32 0x2a+ badSessionKey = B.replicate 31 0x2a+ salt = Salt (B.pack [0x00 .. 0x1f])+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ "payload"+ ]+ packets ct =+ [ PKESKPkt+ ( PKESKPayloadV6Packet+ ( PKESKPayloadV6+ "\x10\x11\x12\x13\x14\x15\x16\x17"+ RSA+ "\x01\x02\x03"+ )+ )+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ct))+ ]+ callback _ = pure (BL.fromStrict badSessionKey)+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ (SessionKey sessionKey)+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ result <-+ catch+ ( Right+ <$> ( DC.runConduitRes $+ CL.sourceList (packets ciphertext)+ DC..| conduitDecrypt callback+ DC..| CL.consume+ )+ )+ (\e -> pure (Left (show (e :: SomeException))))+ case result of+ Left err ->+ if "PKESK raw session key length does not match payload algorithm"+ `isInfixOf` err+ then return ()+ else+ assertFailure+ ( "Expected PKESKv6 raw session-key length validation failure, got: "+ ++ err+ )+ Right got ->+ assertFailure+ ( "Expected PKESKv6 raw session-key length failure, got packets: "+ ++ show got+ )++testConduitDecryptSEIPDv2WithPKESKRSAUnwrap :: Assertion+testConduitDecryptSEIPDv2WithPKESKRSAUnwrap = do+ (baseRecipient, privateKey) <- loadUnencryptedRsaSigner+ publicKey <-+ case _pubkey baseRecipient of+ RSAPubKey (RSA_PublicKey pub) -> pure pub+ other ->+ assertFailure+ ( "unencrypted.seckey did not contain an RSA recipient, got "+ ++ show other+ )+ >> fail "expected RSA recipient"+ let sessionKey = B.replicate 32 0x2b+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk rsa unwrap decrypt path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ = do+ let msk = Just (RSAPrivateKey (RSA_PrivateKey privateKey))+ pure $+ fmap+ ( \sk ->+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Nothing+ , pkeskRecipientSKey = sk+ }+ )+ msk+ encryptedResult <-+ ( P15.encrypt publicKey sessionKey+ :: IO (Either RSA.Error B.ByteString)+ )+ encryptedSessionMaterial <-+ case encryptedResult of+ Left err ->+ assertFailure ("RSA PKESK encryption failed: " ++ show err)+ >> pure mempty+ Right ct -> pure ct+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ (SessionKey sessionKey)+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ let pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId "\x11\x22\x33\x44\x55\x66\x77\x88")+ RSA+ (MPI (os2ip encryptedSessionMaterial) :| [])+ )+ )+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual "PKESK RSA unwrap decrypt payload" payload gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptSEIPDv2WithPKESKRSAUnwrapViaWildcardCallbackFallback+ :: Assertion+testConduitDecryptSEIPDv2WithPKESKRSAUnwrapViaWildcardCallbackFallback = do+ (baseRecipient, privateKey) <- loadUnencryptedRsaSigner+ publicKey <-+ case _pubkey baseRecipient of+ RSAPubKey (RSA_PublicKey pub) -> pure pub+ other ->+ assertFailure+ ( "unencrypted.seckey did not contain an RSA recipient, got "+ ++ show other+ )+ >> fail "expected RSA recipient"+ let sessionKey = B.replicate 32 0x2e+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk rsa unwrap decrypt path via wildcard callback fallback"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback pkt =+ case pkt of+ PKESKPkt+ (PKESKPayloadV3Packet (PKESKPayloadV3 _ (EightOctetKeyId rid) _ _))+ | BL.length rid == 8 && BL.all (== 0) rid ->+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Nothing+ , pkeskRecipientSKey =+ RSAPrivateKey (RSA_PrivateKey privateKey)+ }+ )+ )+ _ -> pure Nothing+ encryptedResult <-+ ( P15.encrypt publicKey sessionKey+ :: IO (Either RSA.Error B.ByteString)+ )+ encryptedSessionMaterial <-+ case encryptedResult of+ Left err ->+ assertFailure ("RSA PKESK encryption failed: " ++ show err)+ >> pure mempty+ Right ct -> pure ct+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ (SessionKey sessionKey)+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ let pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId "\xaa\xbb\xcc\xdd\xee\xff\x00\x11")+ RSA+ (MPI (os2ip encryptedSessionMaterial) :| [])+ )+ )+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "PKESK RSA unwrap via wildcard callback fallback payload"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptSEIPDv2WithPKESKRSAUnwrapFromProtectedKey+ :: Assertion+testConduitDecryptSEIPDv2WithPKESKRSAUnwrapFromProtectedKey = do+ passphrase <- readPKIPassphrase+ secretPackets <-+ DC.runConduitRes $+ CB.sourceFile "tests/data/aes256-sha512.seckey"+ DC..| conduitGet get+ DC..| CL.consume+ keyInfos <- collectSecretKeyInfos secretPackets passphrase+ keyInfo <-+ case keyInfos of+ (ki : _) -> pure ki+ [] ->+ assertFailure+ "aes256-sha512.seckey should yield at least one RSA key context"+ >> fail "expected key info"+ publicKey <-+ case pkeskRecipientPKPayload keyInfo of+ Nothing ->+ assertFailure+ "key info from file should include a public key payload"+ >> fail "expected public key payload"+ Just pkp ->+ case _pubkey pkp of+ RSAPubKey (RSA_PublicKey pub) -> pure pub+ other ->+ assertFailure+ ( "expected RSA public key from aes256-sha512.seckey, got "+ ++ show other+ )+ >> fail "expected RSA public key"+ let sessionKey = B.replicate 32 0x42+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk rsa unwrap via sha1-cfb protected key loaded from file"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback pkt = pure (selectRecipientKeyInfo pkt keyInfos)+ encryptedResult <-+ ( P15.encrypt publicKey sessionKey+ :: IO (Either RSA.Error B.ByteString)+ )+ encryptedSessionMaterial <-+ case encryptedResult of+ Left err ->+ assertFailure ("RSA PKESK encryption failed: " ++ show err)+ >> pure mempty+ Right ct -> pure ct+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ (SessionKey sessionKey)+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ let pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId "\x11\x22\x33\x44\x55\x66\x77\x88")+ RSA+ (MPI (os2ip encryptedSessionMaterial) :| [])+ )+ )+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithPKESKContext+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "PKESK RSA unwrap with SHA1-CFB protected key from file"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptSEIPDv2WithPKESKECDHUnwrap :: Assertion+testConduitDecryptSEIPDv2WithPKESKECDHUnwrap = do+ let (recipientPub, recipientPriv) = syntheticNISTP256Low+ (ephPub, ephPriv) = syntheticNISTP256High+ recipientPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ ECDH+ ( ECDHPubKey+ (ECDSAPubKey (ECDSA_PublicKey recipientPub))+ SHA256+ AES128+ )+ recipientSKey = ECDHPrivateKey (ECDSA_PrivateKey recipientPriv)+ sessionKey = B.replicate 32 0x2c+ encodedSession =+ B.singleton (fromFVal AES256)+ <> sessionKey+ <> encodeChecksum16 sessionKey+ <> B.replicate 5 0+ sharedSecret =+ BA.convert+ ( ECCDH.getShared+ (ECDSA.public_curve recipientPub)+ (ECDSA.private_d ephPriv)+ (ECDSA.public_q recipientPub)+ )+ :: B.ByteString+ kdfParam =+ buildECDHKDFParamForTest+ recipientPKP+ ECDH+ (ECDSA.public_curve recipientPub)+ SHA256+ AES128+ kek = deriveECDHKekForTest SHA256 AES128 sharedSecret kdfParam+ wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek encodedSession+ ephPointBytes =+ maybe+ (error "failed to encode ephemeral point")+ id+ (point2MBS (ECDSA.public_q ephPub))+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId "\x99\x88\x77\x66\x55\x44\x33\x22")+ ECDH+ (MPI (os2ip ephPointBytes) :| [MPI (os2ip wrappedSession)])+ )+ )+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk ecdh unwrap decrypt path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = recipientSKey+ }+ )+ )+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ (SessionKey sessionKey)+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "PKESK ECDH unwrap decrypt payload"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptSEIPDv2RejectsECDHWrongEphemeralPointLength+ :: Assertion+testConduitDecryptSEIPDv2RejectsECDHWrongEphemeralPointLength = do+ let (recipientPub, recipientPriv) = syntheticNISTP256Low+ (ephPub, ephPriv) = syntheticNISTP256High+ recipientPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ ECDH+ ( ECDHPubKey+ (ECDSAPubKey (ECDSA_PublicKey recipientPub))+ SHA256+ AES128+ )+ recipientSKey = ECDHPrivateKey (ECDSA_PrivateKey recipientPriv)+ sessionKey = B.replicate 32 0x2c+ encodedSession =+ B.singleton (fromFVal AES256)+ <> sessionKey+ <> encodeChecksum16 sessionKey+ <> B.replicate 5 0+ sharedSecret =+ BA.convert+ ( ECCDH.getShared+ (ECDSA.public_curve recipientPub)+ (ECDSA.private_d ephPriv)+ (ECDSA.public_q recipientPub)+ )+ :: B.ByteString+ kdfParam =+ buildECDHKDFParamForTest+ recipientPKP+ ECDH+ (ECDSA.public_curve recipientPub)+ SHA256+ AES128+ kek = deriveECDHKekForTest SHA256 AES128 sharedSecret kdfParam+ wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek encodedSession+ ephPointBytes =+ maybe+ (error "failed to encode ephemeral point")+ id+ (point2MBS (ECDSA.public_q ephPub))+ truncatedEphemeral = B.take (B.length ephPointBytes - 1) ephPointBytes+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId "\x99\x88\x77\x66\x55\x44\x33\x22")+ ECDH+ (MPI (os2ip truncatedEphemeral) :| [MPI (os2ip wrappedSession)])+ )+ )+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk ecdh wrong ephemeral point length"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = recipientSKey+ }+ )+ )+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ (SessionKey sessionKey)+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ result <-+ catch+ ( Right+ <$> DC.runConduitRes+ ( CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ )+ )+ (\e -> pure (Left (show (e :: SomeException))))+ case result of+ Left err ->+ assertBool+ "ECDH unwrap should reject wrong uncompressed point length for recipient curve"+ ("invalid length for recipient curve" `isInfixOf` err)+ Right packets ->+ assertFailure+ ( "Expected ECDH point-length validation failure, got: "+ ++ show packets+ )++testConduitDecryptSEIPDv2WithPKESKX25519V3Unwrap :: Assertion+testConduitDecryptSEIPDv2WithPKESKX25519V3Unwrap = do+ let recipientSecretRaw = B.pack [0x21 .. 0x40]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient secret key: " ++ show err)+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ recipientPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip recipientPublicRaw)))+ )+ recipientSKey = X25519PrivateKey recipientSecretRaw+ ephSecretRaw = B.pack [0x41 .. 0x60]+ ephSecret =+ case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 ephemeral secret key: " ++ show err)+ Right sk -> sk+ ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ recipientPub =+ case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient public key: " ++ show err)+ Right pk -> pk+ sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString+ kek =+ deriveX25519KekForTest+ ephPublicRaw+ recipientPublicRaw+ sharedSecret+ sessionKey = SessionKey (B.replicate 32 0x6a)+ wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)+ eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId "\x12\x34\x56\x78\x90\xab\xcd\xef")+ X25519+ (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])+ )+ )+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk x25519 v3 unwrap decrypt path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = recipientSKey+ }+ )+ )+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "PKESK X25519 v3 unwrap decrypt payload"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptSEIPDv2RetriesWildcardPKESKv3AcrossRecipientKeyOrder+ :: Assertion+testConduitDecryptSEIPDv2RetriesWildcardPKESKv3AcrossRecipientKeyOrder = do+ let recipientSecretRaw = B.pack [0x21 .. 0x40]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient secret key: " ++ show err)+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ recipientPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip recipientPublicRaw)))+ )+ recipientSKey = X25519PrivateKey recipientSecretRaw+ wrongRecipientSecretRaw = B.pack [0x61 .. 0x80]+ wrongRecipientSecret =+ case CE.eitherCryptoError (C25519.secretKey wrongRecipientSecretRaw) of+ Left err ->+ error+ ( "failed to initialize wrong X25519 recipient secret key: "+ ++ show err+ )+ Right sk -> sk+ wrongRecipientPublicRaw =+ BA.convert (C25519.toPublic wrongRecipientSecret) :: B.ByteString+ wrongRecipientPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip wrongRecipientPublicRaw)))+ )+ wrongRecipientSKey = X25519PrivateKey wrongRecipientSecretRaw+ ephSecretRaw = B.pack [0x41 .. 0x60]+ ephSecret =+ case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 ephemeral secret key: " ++ show err)+ Right sk -> sk+ ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ recipientPub =+ case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient public key: " ++ show err)+ Right pk -> pk+ sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString+ kek =+ deriveX25519KekForTest+ ephPublicRaw+ recipientPublicRaw+ sharedSecret+ sessionKey = SessionKey (B.replicate 32 0x6b)+ wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)+ eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId (BL.replicate 8 0))+ X25519+ (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])+ )+ )+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk x25519 v3 wildcard key-id retry path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ wrongKeyInfo =+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Just wrongRecipientPKP+ , pkeskRecipientSKey = wrongRecipientSKey+ }+ correctKeyInfo =+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = recipientSKey+ }+ keyContextCallback _rid _pka = pure [wrongKeyInfo, correctKeyInfo]+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithCandidatesCallbackAndPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "PKESKv3 wildcard-keyid retries across recipient key order"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptSEIPDv2RetriesWildcardPKESKv3AcrossLongRecipientKeyOrder+ :: Assertion+testConduitDecryptSEIPDv2RetriesWildcardPKESKv3AcrossLongRecipientKeyOrder = do+ let recipientSecretRaw = B.pack [0x21 .. 0x40]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient secret key: " ++ show err)+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ ephSecretRaw = B.pack [0x41 .. 0x60]+ ephSecret =+ case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 ephemeral secret key: " ++ show err)+ Right sk -> sk+ ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ recipientPub =+ case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient public key: " ++ show err)+ Right pk -> pk+ sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString+ kek =+ deriveX25519KekForTest+ ephPublicRaw+ recipientPublicRaw+ sharedSecret+ sessionKey = SessionKey (B.replicate 32 0x6c)+ wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)+ eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId (BL.replicate 8 0))+ X25519+ (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])+ )+ )+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk x25519 v3 wildcard key-id long retry path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ mkSecretBytes offset =+ B.pack+ [fromIntegral ((offset + i) `mod` 256) | i <- [0 .. 31 :: Int]]+ mkX25519RecipientKeyInfo label secretRaw =+ let sk =+ case CE.eitherCryptoError (C25519.secretKey secretRaw) of+ Left err ->+ error+ ( "failed to initialize "+ ++ label+ ++ " X25519 secret key: "+ ++ show err+ )+ Right secretKey -> secretKey+ publicRaw = BA.convert (C25519.toPublic sk) :: B.ByteString+ in PKESKRecipientKey+ { pkeskRecipientPKPayload =+ Just+ ( PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip publicRaw)))+ )+ )+ , pkeskRecipientSKey = X25519PrivateKey secretRaw+ }+ wrongKeyInfos =+ [ mkX25519RecipientKeyInfo+ ("wrong-" ++ show n)+ (mkSecretBytes (96 + n * 7))+ | n <- [1 .. 20 :: Int]+ ]+ correctKeyInfo = mkX25519RecipientKeyInfo "correct" recipientSecretRaw+ keyContextCallback _rid _pka = pure (wrongKeyInfos ++ [correctKeyInfo])+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithCandidatesCallbackAndPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "PKESKv3 wildcard-keyid retries long recipient key order"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptSEIPDv1RetriesWildcardPKESKv3AcrossLongRecipientKeyOrder+ :: Assertion+testConduitDecryptSEIPDv1RetriesWildcardPKESKv3AcrossLongRecipientKeyOrder = do+ let recipientSecretRaw = B.pack [0x21 .. 0x40]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient secret key: " ++ show err)+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ ephSecretRaw = B.pack [0x41 .. 0x60]+ ephSecret =+ case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 ephemeral secret key: " ++ show err)+ Right sk -> sk+ ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ recipientPub =+ case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient public key: " ++ show err)+ Right pk -> pk+ sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString+ kek =+ deriveX25519KekForTest+ ephPublicRaw+ recipientPublicRaw+ sharedSecret+ sessionKey = SessionKey (B.replicate 32 0x6d)+ wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)+ eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId (BL.replicate 8 0))+ X25519+ (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])+ )+ )+ payload =+ "pkesk x25519 v3 wildcard key-id long retry legacy seipd1 path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ cleartext = BL.toStrict (runPut (put literalBlock))+ iv = IV (B.pack [0x33 .. 0x42])+ cleartextWithMDC = cleartext <> mdcTrailerForSEIPDv1 iv cleartext+ passphraseCallback _ = pure BL.empty+ mkSecretBytes offset =+ B.pack+ [fromIntegral ((offset + i) `mod` 256) | i <- [0 .. 31 :: Int]]+ mkX25519RecipientKeyInfo label secretRaw =+ let sk =+ case CE.eitherCryptoError (C25519.secretKey secretRaw) of+ Left err ->+ error+ ( "failed to initialize "+ ++ label+ ++ " X25519 secret key: "+ ++ show err+ )+ Right secretKey -> secretKey+ publicRaw = BA.convert (C25519.toPublic sk) :: B.ByteString+ in PKESKRecipientKey+ { pkeskRecipientPKPayload =+ Just+ ( PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip publicRaw)))+ )+ )+ , pkeskRecipientSKey = X25519PrivateKey secretRaw+ }+ wrongKeyInfos =+ [ mkX25519RecipientKeyInfo+ ("wrong-" ++ show n)+ (mkSecretBytes (96 + n * 7))+ | n <- [1 .. 20 :: Int]+ ]+ correctKeyInfo = mkX25519RecipientKeyInfo "correct" recipientSecretRaw+ keyContextCallback _rid _pka = pure (wrongKeyInfos ++ [correctKeyInfo])+ ciphertext <-+ either+ ( \e ->+ assertFailure ("encryptOpenPGPCfbRaw failed: " ++ show e)+ >> pure mempty+ )+ pure+ ( encryptOpenPGPCfbRaw+ OpenPGPCFBNoResyncW+ AES256+ iv+ cleartextWithMDC+ (unSessionKey sessionKey)+ )+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD1 1 (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithCandidatesCallbackAndPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "PKESKv3 wildcard-keyid retries long recipient key order for SEIPDv1"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testMkCandidateResolverDecryptsWildcardPKESKv3 :: Assertion+testMkCandidateResolverDecryptsWildcardPKESKv3 = do+ let recipientSecretRaw = B.pack [0x21 .. 0x40]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient secret key: " ++ show err)+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ recipientPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip recipientPublicRaw)))+ )+ recipientSKey = X25519PrivateKey recipientSecretRaw+ wrongSecretRaw = B.pack [0x61 .. 0x80]+ wrongSecret =+ case CE.eitherCryptoError (C25519.secretKey wrongSecretRaw) of+ Left err ->+ error+ ("failed to initialize wrong X25519 secret key: " ++ show err)+ Right sk -> sk+ wrongPublicRaw = BA.convert (C25519.toPublic wrongSecret) :: B.ByteString+ wrongPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip wrongPublicRaw)))+ )+ wrongSKey = X25519PrivateKey wrongSecretRaw+ ephSecretRaw = B.pack [0x41 .. 0x60]+ ephSecret =+ case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 ephemeral secret key: " ++ show err)+ Right sk -> sk+ ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ recipientPub =+ case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient public key: " ++ show err)+ Right pk -> pk+ sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString+ kek =+ deriveX25519KekForTest+ ephPublicRaw+ recipientPublicRaw+ sharedSecret+ sessionKey = SessionKey (B.replicate 32 0x6d)+ wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)+ eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId (BL.replicate 8 0))+ X25519+ (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])+ )+ )+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "unwrap callback wildcard path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ wrongKeyInfo =+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Just wrongPKP+ , pkeskRecipientSKey = wrongSKey+ }+ correctKeyInfo =+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = recipientSKey+ }+ let resolver _rid _pka = pure [wrongKeyInfo, correctKeyInfo]+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| ( void $+ DCD.conduitDecrypt+ DCD.DecryptOptions+ { DCD.decryptOptionsKeyResolution =+ DCD.DecryptWithUnwrapCandidatesCallback resolver+ , DCD.decryptOptionsPolicy = lenientDecryptPolicy+ , DCD.decryptOptionsPassphraseCallback = passphraseCallback+ }+ )+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "unwrap callback should decrypt wildcard PKESK via candidate list"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptWildcardResolverProvidesTypedPreviousFailures+ :: Assertion+testConduitDecryptWildcardResolverProvidesTypedPreviousFailures = do+ let recipientSecretRaw = B.pack [0x21 .. 0x40]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient secret key: " ++ show err)+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ recipientPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip recipientPublicRaw)))+ )+ recipientSKey = X25519PrivateKey recipientSecretRaw+ wrongSecretRaw = B.pack [0x61 .. 0x80]+ wrongSecret =+ case CE.eitherCryptoError (C25519.secretKey wrongSecretRaw) of+ Left err ->+ error+ ("failed to initialize wrong X25519 secret key: " ++ show err)+ Right sk -> sk+ wrongPublicRaw = BA.convert (C25519.toPublic wrongSecret) :: B.ByteString+ wrongPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip wrongPublicRaw)))+ )+ wrongSKey = X25519PrivateKey wrongSecretRaw+ ephSecretRaw = B.pack [0x41 .. 0x60]+ ephSecret =+ case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 ephemeral secret key: " ++ show err)+ Right sk -> sk+ ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ recipientPub =+ case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient public key: " ++ show err)+ Right pk -> pk+ sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString+ kek =+ deriveX25519KekForTest+ ephPublicRaw+ recipientPublicRaw+ sharedSecret+ sessionKey = SessionKey (B.replicate 32 0x6e)+ wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)+ eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId (BL.replicate 8 0))+ X25519+ (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])+ )+ )+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "typed previous failures for wildcard retries"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ wrongKeyInfo =+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Just wrongPKP+ , pkeskRecipientSKey = wrongSKey+ }+ correctKeyInfo =+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = recipientSKey+ }+ let resolver _rid _pka = pure [wrongKeyInfo, correctKeyInfo]+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| ( void $+ DCD.conduitDecrypt+ DCD.DecryptOptions+ { DCD.decryptOptionsKeyResolution =+ DCD.DecryptWithUnwrapCandidatesCallback resolver+ , DCD.decryptOptionsPolicy = lenientDecryptPolicy+ , DCD.decryptOptionsPassphraseCallback = passphraseCallback+ }+ )+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "wildcard retries with typed previous failures should still decrypt"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)+ pure ()++testConduitDecryptWithReportCapturesWildcardResolverDiagnostics+ :: Assertion+testConduitDecryptWithReportCapturesWildcardResolverDiagnostics = do+ let recipientSecretRaw = B.pack [0x21 .. 0x40]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient secret key: " ++ show err)+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ recipientPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip recipientPublicRaw)))+ )+ recipientSKey = X25519PrivateKey recipientSecretRaw+ wrongSecretRaw = B.pack [0x61 .. 0x80]+ wrongSecret =+ case CE.eitherCryptoError (C25519.secretKey wrongSecretRaw) of+ Left err ->+ error+ ("failed to initialize wrong X25519 secret key: " ++ show err)+ Right sk -> sk+ wrongPublicRaw = BA.convert (C25519.toPublic wrongSecret) :: B.ByteString+ wrongPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip wrongPublicRaw)))+ )+ wrongSKey = X25519PrivateKey wrongSecretRaw+ ephSecretRaw = B.pack [0x41 .. 0x60]+ ephSecret =+ case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 ephemeral secret key: " ++ show err)+ Right sk -> sk+ ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ recipientPub =+ case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient public key: " ++ show err)+ Right pk -> pk+ sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString+ kek =+ deriveX25519KekForTest+ ephPublicRaw+ recipientPublicRaw+ sharedSecret+ sessionKey = SessionKey (B.replicate 32 0x6f)+ wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)+ eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId (BL.replicate 8 0))+ X25519+ (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])+ )+ )+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "conduitDecryptWithReport wildcard diagnostics"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ wrongKeyInfo =+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Just wrongPKP+ , pkeskRecipientSKey = wrongSKey+ }+ correctKeyInfo =+ PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = recipientSKey+ }+ let resolver _rid _pka = pure [wrongKeyInfo, correctKeyInfo]+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ (report, decrypted) <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| DC.fuseBoth+ ( DCD.conduitDecryptWithReport+ DCD.DecryptOptions+ { DCD.decryptOptionsKeyResolution =+ DCD.DecryptWithUnwrapCandidatesCallback resolver+ , DCD.decryptOptionsPolicy = lenientDecryptPolicy+ , DCD.decryptOptionsPassphraseCallback = passphraseCallback+ }+ )+ CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "wildcard retries with report diagnostics should decrypt"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)+ assertEqual+ "decrypt report should preserve outcome"+ DCD.DecryptClean+ (DCD.decryptReportOutcome report)+ case DCD.decryptReportSessionKeyResolutions report of+ [resolution] -> do+ assertEqual+ "resolution path should be PKESK for wildcard key retries"+ DCD.DecryptResolvedViaPKESK+ (DCD.decryptSessionResolutionPath resolution)+ let attempts = DCD.decryptSessionResolutionResolverAttempts resolution+ assertBool+ "report should contain wildcard resolver attempts"+ (length attempts >= 2)+ assertBool+ "report should record typed previous failures in resolver attempt context"+ ( any+ ( \attempt ->+ any+ ( \failure ->+ DCD.pkeskAttemptFailureKind failure+ == DCD.PKESKAttemptUnwrapFailed+ && DCD.pkeskAttemptFailureKeyContext failure == Just (V4, X25519)+ )+ (DCD.pkeskResolverAttemptPreviousFailures attempt)+ )+ attempts+ )+ assertBool+ "report should capture ResolveWith key context for X25519"+ ( any+ ( \attempt ->+ DCD.pkeskResolverAttemptAction attempt+ == DCD.ResolverAttemptResolveWith (Just (V4, X25519))+ )+ attempts+ )+ other ->+ assertFailure+ ( "Expected exactly one session-key resolution report, got "+ ++ show (length other)+ )++testConduitDecryptSEIPDv2RejectsPKESKX25519V3WrongEphemeralLength+ :: Assertion+testConduitDecryptSEIPDv2RejectsPKESKX25519V3WrongEphemeralLength = do+ let recipientSecretRaw = B.pack [0x21 .. 0x40]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient secret key: " ++ show err)+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ recipientPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip recipientPublicRaw)))+ )+ recipientSKey = X25519PrivateKey recipientSecretRaw+ ephSecretRaw = B.pack [0x41 .. 0x60]+ ephSecret =+ case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 ephemeral secret key: " ++ show err)+ Right sk -> sk+ ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ recipientPub =+ case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient public key: " ++ show err)+ Right pk -> pk+ sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString+ kek =+ deriveX25519KekForTest+ ephPublicRaw+ recipientPublicRaw+ sharedSecret+ sessionKey = SessionKey (B.replicate 32 0x6a)+ wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek (unSessionKey sessionKey)+ eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession+ overlongEphemeral = ephPublicRaw <> "\x00\x01"+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId "\x12\x34\x56\x78\x90\xab\xcd\xef")+ X25519+ (MPI (os2ip overlongEphemeral) :| [MPI (os2ip eskWithAlgo)])+ )+ )+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk x25519 v3 wrong ephemeral length"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = recipientSKey+ }+ )+ )+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ result <-+ catch+ ( Right+ <$> DC.runConduitRes+ ( CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ )+ )+ (\e -> pure (Left (show (e :: SomeException))))+ case result of+ Left err ->+ assertBool+ "PKESKv3 X25519 unwrap should reject non-32-byte ephemeral values"+ ( "invalid X25519 ephemeral public key length/prefix"+ `isInfixOf` err+ )+ Right packets ->+ assertFailure+ ( "Expected X25519 ephemeral-length validation failure, got: "+ ++ show packets+ )++testConduitDecryptSEIPDv2FallsBackFromArgon2SKESKToPKESKv3X25519+ :: Assertion+testConduitDecryptSEIPDv2FallsBackFromArgon2SKESKToPKESKv3X25519 = do+ let recipientSecretRaw = B.pack [0x21 .. 0x40]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient secret key: " ++ show err)+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ recipientPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip recipientPublicRaw)))+ )+ recipientSKey = X25519PrivateKey recipientSecretRaw+ ephSecretRaw = B.pack [0x41 .. 0x60]+ ephSecret =+ case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of+ Left err ->+ error+ ("failed to initialize X25519 ephemeral secret key: " ++ show err)+ Right sk -> sk+ ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ recipientPub =+ case CE.eitherCryptoError (C25519.publicKey recipientPublicRaw) of+ Left err ->+ error+ ("failed to initialize X25519 recipient public key: " ++ show err)+ Right pk -> pk+ sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString+ kek =+ deriveX25519KekForTest+ ephPublicRaw+ recipientPublicRaw+ sharedSecret+ sessionKeyBytes = B.replicate 32 0x6a+ sessionKey = SessionKey sessionKeyBytes+ wrappedSession = aesKeyWrapRFC3394ForTest AES128 kek sessionKeyBytes+ eskWithAlgo = B.singleton (fromFVal AES256) <> wrappedSession+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId "\x12\x34\x56\x78\x90\xab\xcd\xef")+ X25519+ (MPI (os2ip ephPublicRaw) :| [MPI (os2ip eskWithAlgo)])+ )+ )+ skeskS2K = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15+ skeskPassphrase = "correct horse battery staple"+ wrongPassphrase = "wrong passphrase"+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "skesk->pkesk fallback decrypt path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = recipientSKey+ }+ )+ )+ passphraseCallback _ = pure wrongPassphrase+ keyLen <-+ case keySize AES256 of+ Left err ->+ assertFailure ("keySize failed for AES256: " ++ show err)+ >> fail "keySize failed"+ Right n -> pure n+ skeskKek <-+ case string2Key skeskS2K keyLen skeskPassphrase of+ Left err ->+ assertFailure+ ("string2Key failed for Argon2 SKESK: " ++ renderS2KError err)+ >> fail "string2Key failed"+ Right x -> pure x+ skeskEncryptedEsk <-+ case withSymmetricCipher AES256 skeskKek $ \cipher ->+ paddedCfbEncrypt+ cipher+ (B.replicate (blockSize cipher) 0)+ (B.singleton (fromFVal AES256) <> sessionKeyBytes) of+ Left err ->+ assertFailure+ ("encrypting Argon2 SKESK ESK failed: " ++ show err)+ >> fail "encrypting SKESK ESK failed"+ Right x -> pure x+ let skesk =+ SKESKPkt+ ( SKESKPayloadV4Packet+ ( SKESKPayloadV4+ AES256+ skeskS2K+ (Just (BL.fromStrict skeskEncryptedEsk))+ )+ )+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ skesk+ , pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "conduitDecrypt should fall back from failing Argon2 SKESK to PKESKv3 X25519"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptSEIPDv2FallsBackToEarlierArgon2SKESK+ :: Assertion+testConduitDecryptSEIPDv2FallsBackToEarlierArgon2SKESK = do+ let skeskS2K = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15+ passphrase = "password"+ sessionKeyBytes = B.replicate 32 0x7b+ sessionKey = SessionKey sessionKeyBytes+ badLatestEsk = BL.fromStrict (B.pack [0x00])+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "earlier argon2 skesk fallback decrypt path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure passphrase+ keyLen <-+ case keySize AES256 of+ Left err ->+ assertFailure ("keySize failed for AES256: " ++ show err)+ >> fail "keySize failed"+ Right n -> pure n+ skeskKek <-+ case string2Key skeskS2K keyLen passphrase of+ Left err ->+ assertFailure+ ("string2Key failed for Argon2 SKESK: " ++ renderS2KError err)+ >> fail "string2Key failed"+ Right x -> pure x+ validEsk <-+ case withSymmetricCipher AES256 skeskKek $ \cipher ->+ paddedCfbEncrypt+ cipher+ (B.replicate (blockSize cipher) 0)+ (B.singleton (fromFVal AES256) <> sessionKeyBytes) of+ Left err ->+ assertFailure+ ("encrypting Argon2 SKESK ESK failed: " ++ show err)+ >> fail "encrypting SKESK ESK failed"+ Right x -> pure x+ let earlierValidSKESK =+ SKESKPkt+ ( SKESKPayloadV4Packet+ (SKESKPayloadV4 AES256 skeskS2K (Just (BL.fromStrict validEsk)))+ )+ latestUnusableSKESK =+ SKESKPkt+ ( SKESKPayloadV4Packet+ (SKESKPayloadV4 AES256 skeskS2K (Just badLatestEsk))+ )+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ earlierValidSKESK+ , latestUnusableSKESK+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ (\_ -> pure Nothing)+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "conduitDecrypt should retry earlier Argon2 SKESK when latest SKESK is unusable"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptSEIPDv2RejectsECDHNonTable30Params :: Assertion+testConduitDecryptSEIPDv2RejectsECDHNonTable30Params = do+ let (recipientPub, recipientPriv) = syntheticNISTP256Low+ (ephPub, ephPriv) = syntheticNISTP256High+ recipientPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ ECDH+ ( ECDHPubKey+ (ECDSAPubKey (ECDSA_PublicKey recipientPub))+ SHA256+ AES256+ )+ recipientSKey = ECDHPrivateKey (ECDSA_PrivateKey recipientPriv)+ sessionKey = B.replicate 32 0x2c+ encodedSession =+ B.singleton (fromFVal AES256)+ <> sessionKey+ <> encodeChecksum16 sessionKey+ <> B.replicate 5 0+ sharedSecret =+ BA.convert+ ( ECCDH.getShared+ (ECDSA.public_curve recipientPub)+ (ECDSA.private_d ephPriv)+ (ECDSA.public_q recipientPub)+ )+ :: B.ByteString+ kdfParam =+ buildECDHKDFParamForTest+ recipientPKP+ ECDH+ (ECDSA.public_curve recipientPub)+ SHA256+ AES256+ kek = deriveECDHKekForTest SHA256 AES256 sharedSecret kdfParam+ wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession+ ephPointBytes =+ maybe+ (error "failed to encode ephemeral point")+ id+ (point2MBS (ECDSA.public_q ephPub))+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId "\x99\x88\x77\x66\x55\x44\x33\x22")+ ECDH+ (MPI (os2ip ephPointBytes) :| [MPI (os2ip wrappedSession)])+ )+ )+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk ecdh unwrap policy failure path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = recipientSKey+ }+ )+ )+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ (SessionKey sessionKey)+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ result <-+ catch+ ( Right+ <$> DC.runConduitRes+ ( CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ )+ )+ (\e -> pure (Left (show (e :: SomeException))))+ case result of+ Left err ->+ assertBool+ "ECDH unwrap should reject non-Table-30 parameters"+ ("Table 30 policy violation" `isInfixOf` err)+ Right packets ->+ assertFailure+ ( "Expected Table 30 policy failure, got decrypted packets: "+ ++ show packets+ )++testConduitDecryptSEIPDv2AllowsV4Curve25519LegacyRFC6637AcceptedParams+ :: Assertion+testConduitDecryptSEIPDv2AllowsV4Curve25519LegacyRFC6637AcceptedParams = do+ let recipientSecretRaw = B.pack [1 .. 32]+ ephSecretRaw = B.pack [101 .. 132]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ( "failed to initialize Curve25519Legacy recipient secret key: "+ ++ show err+ )+ Right sk -> sk+ ephSecret =+ case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of+ Left err ->+ error+ ( "failed to initialize Curve25519Legacy ephemeral secret key: "+ ++ show err+ )+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ recipientPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ ECDH+ ( ECDHPubKey+ ( EdDSAPubKey+ EdSigningCurve25519+ ( PrefixedNativeEPoint+ (EPoint (os2ip (B.cons 0x40 recipientPublicRaw)))+ )+ )+ SHA256+ AES256+ )+ recipientSKey =+ ECDHPrivateKey+ ( ECDSA_PrivateKey+ ( ECDSA.PrivateKey+ (ECCT.getCurveByName ECCT.SEC_p256r1)+ (os2ip recipientSecretRaw)+ )+ )+ sessionKey = B.replicate 32 0x3d+ encodedSession =+ B.singleton (fromFVal AES256)+ <> sessionKey+ <> encodeChecksum16 sessionKey+ <> B.replicate 5 0+ sharedSecret =+ BA.convert+ (C25519.dh (C25519.toPublic ephSecret) recipientSecret)+ :: B.ByteString+ kdfParam =+ buildCurve25519LegacyKdfParamForTest+ recipientPKP+ ECDH+ SHA256+ AES256+ kek = deriveECDHKekForTest SHA256 AES256 sharedSecret kdfParam+ wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId "\x99\x88\x77\x66\x55\x44\x33\x22")+ ECDH+ (MPI (os2ip ephPublicRaw) :| [MPI (os2ip wrappedSession)])+ )+ )+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk ecdh v4 curve25519legacy accepted-combo path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = recipientSKey+ }+ )+ )+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ (SessionKey sessionKey)+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "v4 Curve25519Legacy ECDH should allow RFC6637-accepted parameters"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptSEIPDv2AllowsV4Curve25519LegacyWithTruncatedWrappedMPI+ :: Assertion+testConduitDecryptSEIPDv2AllowsV4Curve25519LegacyWithTruncatedWrappedMPI = do+ let recipientSecretRaw = B.pack [1 .. 32]+ ephSecretRaw = B.pack [101 .. 132]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ( "failed to initialize Curve25519Legacy recipient secret key: "+ ++ show err+ )+ Right sk -> sk+ ephSecret =+ case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of+ Left err ->+ error+ ( "failed to initialize Curve25519Legacy ephemeral secret key: "+ ++ show err+ )+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ recipientPKP =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ ECDH+ ( ECDHPubKey+ ( EdDSAPubKey+ EdSigningCurve25519+ ( PrefixedNativeEPoint+ (EPoint (os2ip (B.cons 0x40 recipientPublicRaw)))+ )+ )+ SHA256+ AES256+ )+ recipientSKey =+ ECDHPrivateKey+ ( ECDSA_PrivateKey+ ( ECDSA.PrivateKey+ (ECCT.getCurveByName ECCT.SEC_p256r1)+ (os2ip recipientSecretRaw)+ )+ )+ sharedSecret =+ BA.convert+ (C25519.dh (C25519.toPublic ephSecret) recipientSecret)+ :: B.ByteString+ kdfParam =+ buildCurve25519LegacyKdfParamForTest+ recipientPKP+ ECDH+ SHA256+ AES256+ kek = deriveECDHKekForTest SHA256 AES256 sharedSecret kdfParam+ candidate =+ find+ ( \(_, wrappedSession) -> not (B.null wrappedSession) && B.head wrappedSession == 0x00+ )+ [ let sessionKeyBytes =+ B.pack+ [ fromIntegral ((seed + offset) `mod` 256)+ | offset <- [0 .. 31]+ ]+ encodedSession =+ B.singleton (fromFVal AES256)+ <> sessionKeyBytes+ <> encodeChecksum16 sessionKeyBytes+ <> B.replicate 5 0+ wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession+ in (SessionKey sessionKeyBytes, wrappedSession)+ | seed <- [0 .. 4095 :: Int]+ ]+ case candidate of+ Nothing ->+ assertFailure+ "failed to find deterministic Curve25519Legacy wrapped session key with leading zero"+ Just (sessionKey, wrappedSession) -> do+ let pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId "\x99\x88\x77\x66\x55\x44\x33\x22")+ ECDH+ (MPI (os2ip ephPublicRaw) :| [MPI (os2ip wrappedSession)])+ )+ )+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk ecdh v4 curve25519legacy truncated wrapped-mpi path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = recipientSKey+ }+ )+ )+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ sessionKey+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "v4 Curve25519Legacy ECDH should unwrap after wrapped-MPI leading-zero truncation"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptSEIPDv2RejectsV6Curve25519LegacyNonTable30Params+ :: Assertion+testConduitDecryptSEIPDv2RejectsV6Curve25519LegacyNonTable30Params = do+ let recipientSecretRaw = B.pack [1 .. 32]+ ephSecretRaw = B.pack [101 .. 132]+ recipientSecret =+ case CE.eitherCryptoError (C25519.secretKey recipientSecretRaw) of+ Left err ->+ error+ ( "failed to initialize Curve25519Legacy recipient secret key: "+ ++ show err+ )+ Right sk -> sk+ ephSecret =+ case CE.eitherCryptoError (C25519.secretKey ephSecretRaw) of+ Left err ->+ error+ ( "failed to initialize Curve25519Legacy ephemeral secret key: "+ ++ show err+ )+ Right sk -> sk+ recipientPublicRaw = BA.convert (C25519.toPublic recipientSecret) :: B.ByteString+ ephPublicRaw = BA.convert (C25519.toPublic ephSecret) :: B.ByteString+ recipientPKP =+ PKPayload+ V6+ (ThirtyTwoBitTimeStamp 0)+ 0+ ECDH+ ( ECDHPubKey+ ( EdDSAPubKey+ EdSigningCurve25519+ (NativeEPoint (EPoint (os2ip recipientPublicRaw)))+ )+ SHA512+ AES256+ )+ recipientSKey =+ ECDHPrivateKey+ ( ECDSA_PrivateKey+ ( ECDSA.PrivateKey+ (ECCT.getCurveByName ECCT.SEC_p256r1)+ (os2ip recipientSecretRaw)+ )+ )+ sessionKey = B.replicate 32 0x3d+ encodedSession =+ B.singleton (fromFVal AES256)+ <> sessionKey+ <> encodeChecksum16 sessionKey+ <> B.replicate 5 0+ sharedSecret =+ BA.convert+ (C25519.dh (C25519.toPublic ephSecret) recipientSecret)+ :: B.ByteString+ kdfParam =+ buildCurve25519LegacyKdfParamForTest+ recipientPKP+ ECDH+ SHA512+ AES256+ kek = deriveECDHKekForTest SHA512 AES256 sharedSecret kdfParam+ wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession+ pkesk =+ PKESKPkt+ ( PKESKPayloadV3Packet+ ( PKESKPayloadV3+ 3+ (EightOctetKeyId "\x99\x88\x77\x66\x55\x44\x33\x22")+ ECDH+ (MPI (os2ip ephPublicRaw) :| [MPI (os2ip wrappedSession)])+ )+ )+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk ecdh v6 curve25519legacy strict path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = recipientSKey+ }+ )+ )+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ (SessionKey sessionKey)+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ result <-+ catch+ ( Right+ <$> DC.runConduitRes+ ( CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithDecryptPolicy+ lenientDecryptPolicy+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ )+ )+ (\e -> pure (Left (show (e :: SomeException))))+ case result of+ Left err ->+ assertBool+ "v6 Curve25519Legacy ECDH should remain strict"+ ("Table 30 policy violation" `isInfixOf` err)+ Right packets ->+ assertFailure+ ( "Expected Table 30 policy failure, got decrypted packets: "+ ++ show packets+ )++testConduitDecryptSEIPDv2WithPKESKX448Unwrap :: Assertion+testConduitDecryptSEIPDv2WithPKESKX448Unwrap = do+ let recipientSecretRaw = B.pack [1 .. 56]+ ephSecretRaw = B.pack [57 .. 112]+ sessionKey = B.replicate 32 0x4d+ encodedSession =+ B.singleton (fromFVal AES256)+ <> sessionKey+ <> encodeChecksum16 sessionKey+ <> B.replicate 5 0+ recipientSecret =+ case CE.eitherCryptoError (C448.secretKey recipientSecretRaw) of+ Left err ->+ error+ ("failed to initialize X448 recipient secret key: " ++ show err)+ Right sk -> sk+ ephSecret =+ case CE.eitherCryptoError (C448.secretKey ephSecretRaw) of+ Left err ->+ error+ ("failed to initialize X448 ephemeral secret key: " ++ show err)+ Right sk -> sk+ recipientPublicRaw = BA.convert (C448.toPublic recipientSecret) :: B.ByteString+ ephPublicRaw = BA.convert (C448.toPublic ephSecret) :: B.ByteString+ sharedSecret =+ BA.convert (C448.dh (C448.toPublic recipientSecret) ephSecret)+ :: B.ByteString+ kek =+ deriveX448KekForTest ephPublicRaw recipientPublicRaw sharedSecret+ wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession+ esk =+ ephPublicRaw+ <> B.singleton (fromIntegral (B.length wrappedSession))+ <> wrappedSession+ recipientPKP =+ PKPayload+ V6+ (ThirtyTwoBitTimeStamp 0)+ 0+ X448+ ( EdDSAPubKey+ EdSigningCurve448+ (NativeEPoint (EPoint (os2ip recipientPublicRaw)))+ )+ recipientRid = unFingerprint (fingerprint recipientPKP)+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk x448 unwrap decrypt path"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = X448PrivateKey recipientSecretRaw+ }+ )+ )+ pkesk =+ PKESKPkt+ ( PKESKPayloadV6Packet+ (PKESKPayloadV6 recipientRid X448 (BL.fromStrict esk))+ )+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ (SessionKey sessionKey)+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ decrypted <-+ DC.runConduitRes $+ CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithPKESKContext+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ case decrypted of+ [LiteralDataPkt _ _ _ gotPayload] ->+ assertEqual+ "PKESK X448 unwrap decrypt payload"+ payload+ gotPayload+ other ->+ assertFailure+ ("Expected one decrypted literal packet, got " ++ show other)++testConduitDecryptSEIPDv2RejectsPKESKX448WrongEphemeralLength+ :: Assertion+testConduitDecryptSEIPDv2RejectsPKESKX448WrongEphemeralLength = do+ let recipientSecretRaw = B.pack [1 .. 56]+ ephSecretRaw = B.pack [57 .. 112]+ sessionKey = B.replicate 32 0x4d+ encodedSession =+ B.singleton (fromFVal AES256)+ <> sessionKey+ <> encodeChecksum16 sessionKey+ <> B.replicate 5 0+ recipientSecret =+ case CE.eitherCryptoError (C448.secretKey recipientSecretRaw) of+ Left err ->+ error+ ("failed to initialize X448 recipient secret key: " ++ show err)+ Right sk -> sk+ ephSecret =+ case CE.eitherCryptoError (C448.secretKey ephSecretRaw) of+ Left err ->+ error+ ("failed to initialize X448 ephemeral secret key: " ++ show err)+ Right sk -> sk+ recipientPublicRaw = BA.convert (C448.toPublic recipientSecret) :: B.ByteString+ ephPublicRaw = BA.convert (C448.toPublic ephSecret) :: B.ByteString+ shortEphemeral = B.tail ephPublicRaw+ sharedSecret =+ BA.convert (C448.dh (C448.toPublic recipientSecret) ephSecret)+ :: B.ByteString+ kek =+ deriveX448KekForTest ephPublicRaw recipientPublicRaw sharedSecret+ wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession+ esk =+ shortEphemeral+ <> B.singleton (fromIntegral (B.length wrappedSession))+ <> wrappedSession+ recipientPKP =+ PKPayload+ V6+ (ThirtyTwoBitTimeStamp 0)+ 0+ X448+ ( EdDSAPubKey+ EdSigningCurve448+ (NativeEPoint (EPoint (os2ip recipientPublicRaw)))+ )+ recipientRid = unFingerprint (fingerprint recipientPKP)+ salt = Salt (B.pack [0x00 .. 0x1f])+ payload = "pkesk x448 wrong ephemeral length"+ literalBlock =+ Block+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ payload+ ]+ passphraseCallback _ = pure BL.empty+ keyContextCallback _ =+ pure+ ( Just+ ( PKESKRecipientKey+ { pkeskRecipientPKPayload = Just recipientPKP+ , pkeskRecipientSKey = X448PrivateKey recipientSecretRaw+ }+ )+ )+ pkesk =+ PKESKPkt+ ( PKESKPayloadV6Packet+ (PKESKPayloadV6 recipientRid X448 (BL.fromStrict esk))+ )+ ciphertext <-+ case encryptSEIPDv2Payload+ AES256+ OCB+ 6+ salt+ (SessionKey sessionKey)+ (BL.toStrict (runPut (put literalBlock))) of+ Left err ->+ assertFailure ("encryptSEIPDv2Payload failed: " ++ err)+ >> pure mempty+ Right ct -> pure ct+ result <-+ catch+ ( Right+ <$> DC.runConduitRes+ ( CL.sourceList+ [ pkesk+ , SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 6 salt (BL.fromStrict ciphertext))+ ]+ DC..| conduitDecryptWithPKESKContext+ keyContextCallback+ passphraseCallback+ DC..| CL.consume+ )+ )+ (\e -> pure (Left (show (e :: SomeException))))+ case result of+ Left err ->+ assertBool+ "PKESKv6 X448 unwrap should reject non-56-byte ephemeral values"+ ("expected 56-octet ephemeral value" `isInfixOf` err)+ Right packets ->+ assertFailure+ ( "Expected X448 ephemeral-length validation failure, got: "+ ++ show packets+ )++testDecodeOpenPGPEncodedSessionKeyRejectsTooShort :: Assertion+testDecodeOpenPGPEncodedSessionKeyRejectsTooShort =+ assertEqual+ "encoded session key should reject too-short payloads"+ (Left EncodedSessionKeyTooShort)+ (decodeOpenPGPEncodedSessionKey (B.pack [0x09, 0x01]))++testDecodeOpenPGPEncodedSessionKeyRejectsLengthMismatch+ :: Assertion+testDecodeOpenPGPEncodedSessionKeyRejectsLengthMismatch =+ assertEqual+ "encoded session key should reject algorithm/key-length mismatches"+ (Left (EncodedSessionKeyLengthMismatch AES128 16 3))+ ( decodeOpenPGPEncodedSessionKey+ (B.pack [fromFVal AES128, 0x01, 0x02, 0x03])+ )++testDecodeOpenPGPEncodedSessionKeyRejectsChecksumMismatch+ :: Assertion+testDecodeOpenPGPEncodedSessionKeyRejectsChecksumMismatch =+ assertEqual+ "encoded session key should reject checksum mismatches"+ (Left EncodedSessionKeyChecksumMismatch)+ ( decodeOpenPGPEncodedSessionKey+ (B.pack ([fromFVal AES128] <> replicate 16 0x00 <> [0x00, 0x01]))+ )++testSKESKRejectsUnknownVersion :: Assertion+testSKESKRejectsUnknownVersion = do+ let s2k = Simple SHA256+ pkt =+ SKESKPkt+ (SKESKPayloadV4Packet (SKESKPayloadV4 AES128 s2k Nothing))+ encoded = BL.toStrict (runPut (put pkt))+ malformed =+ BL.fromStrict+ (B.take 2 encoded <> B.singleton 5 <> B.drop 3 encoded)+ case runGet (get :: Get Pkt) malformed of+ Right (BrokenPacketPkt errReason 3 _) ->+ assertBool+ ("expected unsupported SKESK version error, got: " ++ errReason)+ ("unsupported SKESK packet version" `isInfixOf` errReason)+ Right other ->+ assertFailure+ ( "unknown SKESK version should not parse successfully: "+ ++ show other+ )+ Left err ->+ assertFailure+ ( "unknown SKESK version should be reported as a broken packet: "+ ++ err+ )++testSKESK4EncryptedSessionKeyRejectsSimpleS2K :: Assertion+testSKESK4EncryptedSessionKeyRejectsSimpleS2K = do+ let pkt =+ SKESKPkt+ ( SKESKPayloadV4Packet+ ( SKESKPayloadV4+ AES128+ (Simple SHA256)+ (Just (BL.pack [0x01, 0x02, 0x03]))+ )+ )+ encoded = runPut (put pkt)+ case runGet (get :: Get Pkt) encoded of+ Right (BrokenPacketPkt errReason 3 _) ->+ assertBool+ ("expected Simple S2K rejection, got: " ++ errReason)+ ("must not use Simple S2K" `isInfixOf` errReason)+ Right other ->+ assertFailure+ ( "Simple-S2K v4 SKESK with encrypted session key should not parse successfully: "+ ++ show other+ )+ Left err ->+ assertFailure+ ( "Simple-S2K v4 SKESK with encrypted session key should be reported as a broken packet: "+ ++ err+ )++testSKESK4Argon2EncryptedSessionKeyRoundTripAcrossAES+ :: Assertion+testSKESK4Argon2EncryptedSessionKeyRoundTripAcrossAES =+ mapM_ assertRoundTrip [AES128, AES192, AES256]+ where+ passphrase = "password"+ s2k = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15+ assertRoundTrip sa = do+ keyLen <-+ case keySize sa of+ Left err ->+ assertFailure+ ("keySize failed for " ++ show sa ++ ": " ++ show err)+ >> fail "keySize failed"+ Right n -> pure n+ kek <-+ case string2Key s2k keyLen passphrase of+ Left err ->+ assertFailure+ ("string2Key failed for " ++ show sa ++ ": " ++ renderS2KError err)+ >> fail "string2Key failed"+ Right x -> pure x+ let sessionKey = B.pack (take keyLen (cycle [0x11, 0x22, 0x33, 0x44]))+ encodedSessionKey = B.singleton (fromFVal sa) <> sessionKey+ encryptedEsk <-+ case withSymmetricCipher sa kek $ \cipher ->+ paddedCfbEncrypt+ cipher+ (B.replicate (blockSize cipher) 0)+ encodedSessionKey of+ Left err ->+ assertFailure+ ( "encrypting SKESK v4 ESK failed for "+ ++ show sa+ ++ ": "+ ++ show err+ )+ >> fail "encrypting SKESK v4 ESK failed"+ Right x -> pure x+ let skesk = SKESK4Packet sa s2k (Just (BL.fromStrict encryptedEsk))+ case skesk2SessionKey skesk passphrase of+ Right (decodedAlgo, decodedSessionKey) -> do+ assertEqual+ ("decoded SKESK v4 encrypted ESK algorithm for " ++ show sa)+ sa+ decodedAlgo+ assertEqual+ ("decoded SKESK v4 encrypted ESK session key for " ++ show sa)+ sessionKey+ decodedSessionKey+ Left err ->+ assertFailure+ ( "decoding SKESK v4 encrypted ESK failed for "+ ++ show sa+ ++ ": "+ ++ renderS2KError err+ )++testSKESK4Argon2EncryptedSessionKeyRejectsTrailingChecksum+ :: Assertion+testSKESK4Argon2EncryptedSessionKeyRejectsTrailingChecksum = do+ let sa = AES128+ passphrase = "password"+ s2k = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15+ sessionKey = B.pack (take 16 (cycle [0xaa, 0xbb, 0xcc, 0xdd]))+ keyLen <-+ case keySize sa of+ Left err ->+ assertFailure ("keySize failed: " ++ show err)+ >> fail "keySize failed"+ Right n -> pure n+ kek <-+ case string2Key s2k keyLen passphrase of+ Left err ->+ assertFailure ("string2Key failed: " ++ renderS2KError err)+ >> fail "string2Key failed"+ Right x -> pure x+ let encodedWithChecksum =+ B.singleton (fromFVal sa)+ <> sessionKey+ <> encodeChecksum16 sessionKey+ encryptedEsk <-+ case withSymmetricCipher sa kek $ \cipher ->+ paddedCfbEncrypt+ cipher+ (B.replicate (blockSize cipher) 0)+ encodedWithChecksum of+ Left err ->+ assertFailure+ ("encrypting malformed SKESK v4 ESK failed: " ++ show err)+ >> fail "encrypting malformed SKESK v4 ESK failed"+ Right x -> pure x+ let skesk = SKESK4Packet sa s2k (Just (BL.fromStrict encryptedEsk))+ case skesk2SessionKey skesk passphrase of+ Left+ ( S2KEncryptedSessionKeyDecodeError+ (EncodedSessionKeyLengthMismatch AES128 16 18)+ ) -> pure ()+ Left err ->+ assertFailure+ ( "expected SKESK v4 trailing-checksum rejection, got: "+ ++ renderS2KError err+ )+ Right _ ->+ assertFailure+ "expected SKESK v4 trailing-checksum rejection, but decode succeeded"++testEncryptPassphraseWithPolicyForceV4Interop :: Assertion+testEncryptPassphraseWithPolicyForceV4Interop = do+ let request =+ PassphraseEncryptRequest+ { passphraseEncryptVersionPolicy = PassphraseSKESKForceV4Interop+ , passphraseEncryptSymmetricAlgorithm = AES128+ , passphraseEncryptS2K = Simple SHA256+ , passphraseEncryptPassphrase = "password"+ , passphraseEncryptPayload = "hello"+ , passphraseEncryptSEIPDv1IVOverride =+ Just (IV (B.replicate 16 0x22))+ , passphraseEncryptSEIPDv2AEADOverride = Nothing+ , passphraseEncryptSEIPDv2ChunkSizeOverride = Nothing+ , passphraseEncryptSEIPDv2SaltOverride = Nothing+ }+ result <- encryptPassphraseWithPolicy request+ case result of+ Left err ->+ assertFailure+ ( "Expected passphrase SKESK force-v4 encryption to succeed, got: "+ ++ err+ )+ Right+ ( SKESKPkt (SKESKPayloadV4Packet _)+ : SymEncIntegrityProtectedDataPkt (SEIPD1 _ _)+ : _+ ) ->+ pure ()+ Right packets ->+ assertFailure+ ( "Expected SKESKv4 + SEIPDv1 packet sequence, got: "+ ++ show packets+ )++testEncryptPassphraseWithPolicyPreferV6 :: Assertion+testEncryptPassphraseWithPolicyPreferV6 = do+ let request =+ PassphraseEncryptRequest+ { passphraseEncryptVersionPolicy = PassphraseSKESKPreferV6+ , passphraseEncryptSymmetricAlgorithm = AES128+ , passphraseEncryptS2K = Simple SHA256+ , passphraseEncryptPassphrase = "password"+ , passphraseEncryptPayload = "hello"+ , passphraseEncryptSEIPDv1IVOverride = Nothing+ , passphraseEncryptSEIPDv2AEADOverride = Just OCB+ , passphraseEncryptSEIPDv2ChunkSizeOverride = Just 6+ , passphraseEncryptSEIPDv2SaltOverride =+ Just (Salt (B.replicate 32 0x44))+ }+ result <- encryptPassphraseWithPolicy request+ case result of+ Left err ->+ assertFailure+ ( "Expected passphrase SKESK prefer-v6 encryption to succeed, got: "+ ++ err+ )+ Right+ ( SKESKPkt (SKESKPayloadV6Packet _)+ : SymEncIntegrityProtectedDataPkt (SEIPD2 _ _ _ _ _)+ : _+ ) ->+ pure ()+ Right packets ->+ assertFailure+ ( "Expected SKESKv6 + SEIPDv2 packet sequence, got: "+ ++ show packets+ )++testArgon2S2KVector :: Assertion+testArgon2S2KVector = do+ let s2k = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15+ pass = BLC8.pack "password"+ derivedKeyResult = string2Key s2k 16 pass+ derivedKey <-+ case derivedKeyResult of+ Left err ->+ assertFailure+ ("Argon2 S2K key derivation failed: " ++ renderS2KError err)+ >> fail "Argon2 key derivation failed"+ Right x -> pure x+ let+ hex =+ map toUpper . BLC8.unpack . B16L.encode . BL.fromStrict $+ derivedKey+ assertEqual+ "Argon2 S2K key derivation vector"+ "A3BDFE3814D790F1F366ABA6954EB386"+ hex++testArgon2S2KOrdTotal :: Assertion+testArgon2S2KOrdTotal = do+ let argon2A = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15+ argon2B = Argon2 (Salt16 (B.pack [0x01 .. 0x10])) 1 4 15+ assertEqual+ "Argon2 S2K compares after simple S2K"+ GT+ (compare argon2A (Simple SHA256))+ assertEqual+ "Argon2 S2K compares before unknown S2K types"+ LT+ (compare argon2A (OtherS2K 101 BL.empty))+ assertEqual+ "Argon2 S2K constructor compares lexicographically by fields"+ LT+ (compare argon2A argon2B)
tests/Tests/Keys.hs view
@@ -2,1771 +2,2248 @@ -- 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 TypeApplications #-}--module Tests.Keys (keyAndVerificationTests) where--import Codec.Encryption.OpenPGP.Expirations- ( effectiveKeyPreferencesAtTimestamp- , effectiveUIDPreferencesAtTimestamp- , isTKTimeValid- )-import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)-import Codec.Encryption.OpenPGP.Internal- ( emptyPSC- , lastPrimaryKey- , lastSubkey- )-import Codec.Encryption.OpenPGP.KeyInfo (pkalgoAbbrev, pubkeySize)-import Codec.Encryption.OpenPGP.Message- ( asV6PKPayload- , mkEd25519SignerV6- , mkRSASignerV6- )-import Codec.Encryption.OpenPGP.Policy- ( OpenPGPRFC(..)- , isAllowedPrimaryKeySig- , isAllowedSubkeySig- , isAllowedUIDSig- )-import Codec.Encryption.OpenPGP.SecretKey- ( changePrivateKeyPassphrase- , decryptPrivateKey- , mkUnencryptedSKAddendum- , reinterpretUnknownSKeyForPKPayload- )-import Codec.Encryption.OpenPGP.Serialize- ( getSecretKey- , parsePkts- , putSKeyForPKPayload- )-import Codec.Encryption.OpenPGP.SerializeForSigs (payloadForSig)-import Codec.Encryption.OpenPGP.Signatures-import Codec.Encryption.OpenPGP.Subpackets- ( sigBuilderInit- , sigBuilderInitRuntime- , sigBuilderInitV6- , sigBuilderInitV6Runtime- , addHashedSubs- , addUnhashedSubs- , LegalSubpacket(..)- , listToHashedSubs- , listToUnhashedSubs- , listToLegalSubs- )-import Codec.Encryption.OpenPGP.Types-import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as PKA-import Control.Error.Util (isRight)-import Crypto.Number.Serialize (os2ip)-import qualified Crypto.PubKey.ECC.ECDSA as ECDSA-import Data.Bifunctor (first)-import Data.Binary (get, put)-import Data.Binary.Get (Get, runGetOrFail)-import Data.Binary.Put (runPut)-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import qualified Data.Conduit as DC-import qualified Data.Conduit.Binary as CB-import qualified Data.Conduit.List as CL-import Data.Conduit.OpenPGP.Compression (conduitDecompress)-import Data.Conduit.OpenPGP.Keyring (conduitToUnknownTKs)-import Data.Conduit.OpenPGP.Verify (conduitVerify)-import Data.Conduit.Serialization.Binary (conduitGet)-import Data.IxSet.Typed ((@=), getOne, size)-import Data.List (find, isInfixOf)-import Data.List.NonEmpty (NonEmpty(..))-import Data.Maybe (isJust)-import qualified Data.Set as Set-import Data.Time.Clock.POSIX (posixSecondsToUTCTime)-import Prettyprinter (pretty)-import Test.Tasty (TestTree, testGroup)-import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, testCase)-import Tests.Common- ( assertSingleFailureContainsTimeline- , assertSingleSignerFingerprint- , loadAndDecompressPkts- , messageIssuerSubpacketsAt- , addTimestampSeconds- , armorPayload- , assertFalse- , assertTrue- , certificateVerificationFixtures- , fixturePath- , loadArmor- , loadDeterministicEd25519Signer- , loadKeyring- , loadUnencryptedRsaSigner- , loadUnencryptedRsaSignerV6- , messageVerificationFixtures- , mkTestKeyring- , readFixturePayload- , readPKIPassphrase- , runGet- , setKeyVersion- , signBinaryMessageWithRSAAt- , signCertificationAt- , signCertificationRevocationAt- , signCertificationRevocationWithEd25519At- , signCertificationWithEd25519At- , signSubkeyBindingWithRSAExtrasAt- , timestampToUTCTime- , verificationFixtureGroup- , verifyMessageFromBytestring- , verifyMessageFromBytestringBatch- , verifyMessageFromPackets- , verifyMessageFromPacketsBatch- , verifyTimelinePackets- )---keyAndVerificationTests :: TestTree-keyAndVerificationTests =- testGroup- "Keys and verification"- [ testGroup- "PKA/Size/KeyID/fingerprint group"- [ testCase- "v3 key"- (testPKAandSizeAndKeyIDandFingerprint- "v3.key"- "rsa/1024:6DC580F5C7261095/CBD9 F412 6807 E405 CC2D 2712 1DF5 E86E")- , testCase- "v4 key"- (testPKAandSizeAndKeyIDandFingerprint- "000001-006.public_key"- "rsa/1248:D4D54EA16F87040E/421F 28FE AAD2 22F8 56C8 FFD5 D4D5 4EA1 6F87 040E")- , testCase- "ECDSA key"- (testPKAandSizeAndKeyIDandFingerprint- "nist_p-256_key.gpg"- "ecd/256:F7708BADD6063224/174C CF12 C571 6D0E 527F B50E F770 8BAD D606 3224")- , testCase- "EdDSA key"- (testPKAandSizeAndKeyIDandFingerprint- "sample-eddsa.pubkey"- "edd/256:8CFDE12197965A9A/C959 BDBA FA32 A2F8 9A15 3B67 8CFD E121 9796 5A9A")- , testCase- "EdDSA secret key (projected to public key packet)"- (testPKAandSizeAndKeyIDandFingerprint- "ed25519.secretkey"- "edd/256:B05F9287601D5914/B6FE 0C23 12EB D832 0238 C252 B05F 9287 601D 5914")- , testCase- "v6 secret key (projected to public key packet)"- (testPKAandSizeAndKeyIDandFingerprint- "v6-secret.pgp.aa"- "e25/256:7106CB6DB27E4FC9/7106 CB6D B27E 4FC9 B050 B02B B316 4DBD 4BBD 7263 D4D2 9E78 A3BB 3BDB B022 2D19")- ]- , testGroup- "Keyring group"- [ testCase- "pubring 7732CF988A63EA86"- (testKeyringLookup "pubring.gpg" "7732CF988A63EA86" True)- , testCase- "pubring 123456789ABCDEF0"- (testKeyringLookup "pubring.gpg" "123456789ABCDEF0" False)- , testCase- "pubsub AD992E9C24399832"- (testKeyringLookup "pubring.gpg" "AD992E9C24399832" True)- , testCase- "secring 7732CF988A63EA86"- (testKeyringLookup "secring.gpg" "7732CF988A63EA86" True)- , testCase- "secring 123456789ABCDEF0"- (testKeyringLookup "secring.gpg" "123456789ABCDEF0" False)- , testCase- "secsub AD992E9C24399832"- (testKeyringLookup "secring.gpg" "AD992E9C24399832" True)- , testCase "pubring.gpg has 4 keys" (testKeyringSize "pubring.gpg" 4)- , testCase "secring.gpg has 4 keys" (testKeyringSize "secring.gpg" 4)- ]- , verificationFixtureGroup- "Message verification group (packet-based)"- verifyMessageFromPackets- messageVerificationFixtures- , verificationFixtureGroup- "Message verification group (packet-based, batch)"- verifyMessageFromPacketsBatch- messageVerificationFixtures- , verificationFixtureGroup- "Message verification group (bytestring-based)"- verifyMessageFromBytestring- messageVerificationFixtures- , verificationFixtureGroup- "Message verification group (bytestring-based, batch)"- verifyMessageFromBytestringBatch- messageVerificationFixtures- , verificationFixtureGroup- "Certificate verification group (packet-based)"- verifyMessageFromPackets- certificateVerificationFixtures- , verificationFixtureGroup- "Certificate verification group (packet-based, batch)"- verifyMessageFromPacketsBatch- certificateVerificationFixtures- , verificationFixtureGroup- "Certificate verification group (bytestring-based)"- verifyMessageFromBytestring- certificateVerificationFixtures- , verificationFixtureGroup- "Certificate verification group (bytestring-based, batch)"- verifyMessageFromBytestringBatch- certificateVerificationFixtures- , testGroup- "Key verification group"- [ testCase- "6F87040E pubkey"- (testKeysSelfVerification True "6F87040E.pubkey")- , testCase- "revoked pubkey"- (testKeysSelfVerification False "revoked.pubkey")- , testCase- "expired pubkey"- (testKeysSelfVerification True "expired.pubkey")- , testCase- "nist_p-256 pubkey"- (testKeysSelfVerification True "nist_p-256_key.gpg")- , testCase- "ed25519 pubkey"- (testKeysSelfVerification True "ed25519.pubkey")- ]- , testGroup- "Verification error messaging group"- [ testCase- "missing signing key in keyring"- (testMissingSigningKeyMessage- "ecdsa-key-without-ecdh.pubkey"- "uncompressed-ops-rsa.gpg")- , testCase- "tampered message signature mismatch"- (testTamperedSignatureMessage "pubring.gpg" "uncompressed-ops-rsa.gpg")- , testCase- "revoked certificate reports revocation"- (testRevokedCertificateMessage "revoked.pubkey")- ]- , testGroup- "Key expiration group"- [ testCase "6F87040E pubkey" (testKeysExpiration True "6F87040E.pubkey")- , testCase "expired pubkey" (testKeysExpiration False "expired.pubkey")- , testCase- "nist_p-256 pubkey"- (testKeysExpiration True "nist_p-256_key.gpg")- , testCase- "ed25519-without-curve25519.pubkey"- (testKeysExpiration True "ed25519-without-curve25519.pubkey")- , testCase "ed25519.pubkey" (testKeysExpiration True "ed25519.pubkey")- ]- , testGroup- "misc group"- [ testCase "conduitVerify processes SigV6 packets" testConduitVerifyProcessesSigV6- , testCase "signature builder API (Phase 2)" testSignatureBuilders- , testCase "typed legal subpacket builder API (P2.1)" testLegalSubpacketBuilders- , testCase- "legacy secret key passphrase changes preserve protection"- testChangePrivateKeyPassphraseLegacy- , testCase "change private key passphrase" testChangePrivateKeyPassphraseV6- , testCase- "getSecretKey parses v4 X25519 ECDH key material"- testGetSecretKeyHandlesV4X25519ECDHPubkey- , testCase- "v4 Ed25519 secret-key roundtrip preserves leading zero byte"- testV4Ed25519SecretKeyRoundTripPreservesLeadingZeroByte- , testCase- "v4 Ed448 secret-key roundtrip preserves leading zero byte"- testV4Ed448SecretKeyRoundTripPreservesLeadingZeroByte- , testCase- "mkUnencryptedSKAddendum computes legacy secret-key checksum"- testMkUnencryptedSKAddendumComputesLegacyChecksum- , testCase- "mkUnencryptedSKAddendum sets v6 unencrypted checksum to zero"- testMkUnencryptedSKAddendumUsesV6ChecksumConvention- , testCase- "reinterpretUnknownSKeyForPKPayload decodes EdDSA unknown key material"- testReinterpretUnknownSKeyForPKPayloadDecodesEdDSA- , testCase- "fromPrimaryKeyPktToTKUnknown rejects subkey packets"- testFromPrimaryKeyPktToTKUnknownRejectsSubkey- , testCase "policy signature context validation (RFC9580)" testPolicySignatureContextValidation- , testCase- "temporary validity uses latest effective self-signature window"- testTemporaryKeyValidityRespectsLatestSelfSignature- , testCase- "temporary validity rejects gaps with no effective self-signature"- testTemporaryKeyValidityRejectsSelfSignatureGaps- , testCase- "temporary validity rejects the gap before the first self-signature"- testTemporaryKeyValidityRejectsPreCertificationGap- , testCase- "temporary validity follows temporary self-revocations"- testTemporaryKeyValidityFollowsTemporarySelfRevocation- , testCase- "temporary validity does not revive older self-signatures after newer expiry"- testTemporaryKeyValidityDoesNotReviveOlderSelfSignatures- , testCase- "uid-retired revocation is historical until effective"- testUidRetiredRevocationIsHistorical- , testCase- "third-party certifications do not affect primary key validity windows"- testThirdPartyCertificationDoesNotAffectKeyValidityWindow- , testCase- "certification revocations only remove matching signer certifications"- testCertificationRevocationOnlyRemovesMatchingSigner- , testCase- "preference resolution tracks active self-certification timeline"- testPreferencesAtTimestamp- , testCase- "verifySigWith rejects v6 revocation Issuer key-id subpackets"- testVerifySigWithV6RevocationRejectsLegacyIssuerKeyID- , testCase- "verifySigWith rejects v6 revocation unhashed Issuer key-id subpackets"- testVerifySigWithV6RevocationRejectsLegacyIssuerKeyIDInUnhashed- , testCase- "verifySigWith accepts matching v6 revocation IssuerFingerprint"- testVerifySigWithV6RevocationAcceptsMatchingIssuerFingerprint- , testCase "v6 signer constructor rejects v4 key" testV6SignerConstructorRejectsV4Key- , testCase- "primary key binding rejects unsupported critical hashed subpackets"- testPrimaryKeyBindingRejectsUnsupportedCriticalSubpacket- , testCase- "subkey binding missing back-signature is surfaced as a warning"- testSubkeyBindingMissingBackSignatureProducesWarning- , testCase- "subkey binding rejects unsupported critical hashed subpackets"- testSubkeyBindingRejectsUnsupportedCriticalSubpacket- ]- ]--testPKAandSizeAndKeyIDandFingerprint :: FilePath -> String -> Assertion-testPKAandSizeAndKeyIDandFingerprint fpr kf = do- bs <- readFixturePayload fpr- case runGet (get :: Get Pkt) bs of- Left _ -> assertFailure $ "Decoding of " ++ fpr ++ " broke."- Right pkt ->- case publicKeyPacketOf pkt of- PublicKeyPkt pkp -> do- let pref =- concat- [ pkalgoAbbrev (_pkalgo pkp)- , "/"- , either (const "unknown") show (pubkeySize (_pubkey pkp))- , ":"- , either (const "unknown") (show . pretty) (eightOctetKeyID pkp)- , "/"- ]- assertEqual- ("for " ++ fpr ++ " (spaceless)")- (spaceless kf)- (pref ++ show (pretty (fingerprint pkp)))- assertEqual- ("for " ++ fpr ++ " (spaced)")- kf- (pref ++ show (pretty (SpacedFingerprint (fingerprint pkp))))- _ -> assertFailure "Expected (possibly secret) key packet, got something else."- where- spaceless = filter (/= ' ')--testKeyringLookup :: FilePath -> String -> Bool -> Assertion-testKeyringLookup fpr eok expected = do- kr <- loadKeyring fpr- let foundKey = getOne (kr @= (read eok :: EightOctetKeyId))- assertEqual (eok ++ " in " ++ fpr) expected (isJust foundKey)--testKeyringSize :: FilePath -> Int -> Assertion-testKeyringSize fpr expected = do- kr <- loadKeyring fpr- assertEqual ("key count in " ++ fpr) expected (size kr)--assertLeftContains :: String -> Either VerificationError a -> Assertion-assertLeftContains needle result =- case result of- Left err ->- let rendered = renderVerificationError err- in- if needle `isInfixOf` rendered- then pure ()- else- assertFailure- ("Expected error containing '" ++ needle ++ "', got '" ++ rendered ++ "'.")- Right _ -> assertFailure ("Expected Left containing '" ++ needle ++ "', got Right.")--testMissingSigningKeyMessage :: FilePath -> FilePath -> Assertion-testMissingSigningKeyMessage keyring message = do- kr <- loadKeyring keyring- verification <-- DC.runConduitRes $- CB.sourceFile (fixturePath message) DC..| conduitGet get DC..| conduitDecompress DC..|- conduitVerify kr Nothing DC..|- CL.consume- case verification of- [] -> assertFailure "Expected at least one verification result."- (firstResult:_) ->- assertLeftContains "signing key not found in keyring" firstResult--testTamperedSignatureMessage :: FilePath -> FilePath -> Assertion-testTamperedSignatureMessage keyring message = do- kr <- loadKeyring keyring- packets <- loadAndDecompressPkts message- let tamperedPackets = map tamperLiteral packets- verification <-- DC.runConduitRes $- CL.sourceList tamperedPackets DC..| conduitVerify kr Nothing DC..| CL.consume- case verification of- [] -> assertFailure "Expected at least one verification result."- (firstResult:_) ->- assertLeftContains "signature mismatch" firstResult- where- tamperLiteral (LiteralDataPkt dt fn ts payload) =- LiteralDataPkt dt fn ts (BL.snoc payload 0)- tamperLiteral pkt = pkt--testRevokedCertificateMessage :: FilePath -> Assertion-testRevokedCertificateMessage keyfile = do- ks <-- DC.runConduitRes $- CB.sourceFile ("tests/data/" ++ keyfile) DC..| conduitGet get DC..| conduitToUnknownTKs DC..|- CL.consume- assertLeftContains- "signing key is revoked"- (mapM (verifyUnknownTKWith (verifySigWith (verifyAgainstKeys ks)) Nothing) ks)--testKeysSelfVerification :: Bool -> FilePath -> Assertion-testKeysSelfVerification expectsuccess keyfile = do- ks <-- DC.runConduitRes $- CB.sourceFile ("tests/data/" ++ keyfile) DC..| conduitGet get DC..|- conduitToUnknownTKs DC..|- CL.consume- let verifieds =- mapM (verifyUnknownTKWith (verifySigWith (verifyAgainstKeys ks)) Nothing) ks- assertEqual- (keyfile ++ " self-verification")- expectsuccess- (isRight verifieds)--testKeysExpiration :: Bool -> FilePath -> Assertion-testKeysExpiration expectsuccess keyfile = do- ks <-- DC.runConduitRes $- CB.sourceFile (fixturePath keyfile) DC..| conduitGet get DC..|- conduitToUnknownTKs DC..|- CL.consume- case mapM (verifyUnknownTKWith (verifySigWith (verifyAgainstKeys ks)) Nothing) ks of- Left err ->- assertFailure- (keyfile ++ " key self-verification failed before expiration check: " ++- renderVerificationError err)- Right verifieds -> do- let tvalid =- all- (isTKTimeValid- (posixSecondsToUTCTime (realToFrac (1400000000 :: Integer))))- verifieds- assertEqual (keyfile ++ " key expiration") expectsuccess tvalid--testConduitVerifyProcessesSigV6 :: Assertion-testConduitVerifyProcessesSigV6 = do- kr <- loadKeyring "pubring.gpg"- let sigPayload =- SigV6- BinarySig- RSA- SHA256- (SignatureSalt (BL.replicate 16 0))- []- []- 0- (MPI 0 :| [])- packets =- [ LiteralDataPkt BinaryData BL.empty (ThirtyTwoBitTimeStamp 0) "payload"- , SignaturePkt sigPayload- ]- verification <-- DC.runConduitRes $- CL.sourceList packets DC..| conduitVerify kr Nothing DC..| CL.consume- case verification of- [Left _] -> return ()- other ->- assertFailure- ("Expected one v6 verification result (failure is fine), got " ++- show (length other) ++ " results")--testSubkeyBindingRejectsUnsupportedCriticalSubpacket :: Assertion-testSubkeyBindingRejectsUnsupportedCriticalSubpacket = do- (primarySigner, primarySigningKey) <- loadUnencryptedRsaSigner- (subkeySigner, _) <- loadDeterministicEd25519Signer- let creationTime = addTimestampSeconds (_timestamp primarySigner) 10- (hashed, unhashed) <- messageIssuerSubpacketsAt primarySigner creationTime- let bindingPayload =- payloadForSig- SubkeyBindingSig- emptyPSC- { lastPrimaryKey = PublicKeyPkt primarySigner- , lastSubkey = PublicSubkeyPkt subkeySigner- }- hashedWithUnsupportedCritical =- SigSubPacket True (OtherSigSub 111 "unsupported-critical") : hashed- keyring = [TKUnknown (primarySigner, Nothing) [] [] [] []]- state =- emptyPSC- { lastPrimaryKey = PublicKeyPkt primarySigner- , lastSubkey = PublicSubkeyPkt subkeySigner- }- sigPayload <-- case signDataWithRSA- SubkeyBindingSig- primarySigningKey- hashedWithUnsupportedCritical- unhashed- bindingPayload of- Left err ->- assertFailure ("failed to sign subkey binding with unsupported critical subpacket: " ++ renderSignError err) >>- fail "expected subkey binding signature"- Right sig -> pure sig- case verifySigWith (verifyAgainstKeys keyring) (SignaturePkt sigPayload) state Nothing of- Left (UnsupportedCriticalSubpacket SubkeyBindingSig) -> pure ()- Left err ->- assertFailure- ("Expected unsupported critical subpacket rejection for subkey binding signature, got: " ++- renderVerificationError err)- Right _ ->- assertFailure- "Expected unsupported critical subpacket rejection for subkey binding signature, but verification succeeded"--testSubkeyBindingMissingBackSignatureProducesWarning :: Assertion-testSubkeyBindingMissingBackSignatureProducesWarning = do- (primarySigner, primarySigningKey) <- loadUnencryptedRsaSigner- (subkeySignerRaw, _) <- loadDeterministicEd25519Signer- let subkeySigner = setKeyVersion V6 subkeySignerRaw- creationTime = addTimestampSeconds (_timestamp primarySigner) 10- keyring = [TKUnknown (primarySigner, Nothing) [] [] [] []]- state =- emptyPSC- { lastPrimaryKey = PublicKeyPkt primarySigner- , lastSubkey = PublicSubkeyPkt subkeySigner- }- sigPayload <-- signSubkeyBindingWithRSAExtrasAt- primarySigner- subkeySigner- primarySigningKey- creationTime- [SigSubPacket False (KeyFlags (Set.singleton SignDataKey))]- case verifySigWith (verifyAgainstKeys keyring) (SignaturePkt sigPayload) state Nothing of- Left err ->- assertFailure- ("Expected successful verification with warning for missing back-signature, got: " ++- renderVerificationError err)- Right verification ->- assertBool- "v6 signing-capable subkey binding without embedded back-signature should emit warning"- (MissingSubkeyBackSignatureWarning `elem` _verificationWarnings verification)--testPrimaryKeyBindingRejectsUnsupportedCriticalSubpacket :: Assertion-testPrimaryKeyBindingRejectsUnsupportedCriticalSubpacket = do- (signer, _) <- loadUnencryptedRsaSigner- let sigPayload =- SigV4- PrimaryKeyBindingSig- RSA- SHA256- [SigSubPacket True (OtherSigSub 111 "unsupported-critical")]- []- 0- (MPI 0 :| [])- verifyFn _ _ _ = Right (Verification signer sigPayload [])- state =- emptyPSC- { lastPrimaryKey = PublicKeyPkt signer- , lastSubkey = PublicSubkeyPkt signer- }- case verifySigWith verifyFn (SignaturePkt sigPayload) state Nothing of- Left (UnsupportedCriticalSubpacket PrimaryKeyBindingSig) -> pure ()- Left err ->- assertFailure- ("Expected unsupported critical subpacket rejection for primary key binding signature, got: " ++- renderVerificationError err)- Right _ ->- assertFailure- "Expected unsupported critical subpacket rejection for primary key binding signature, but verification succeeded"--testSignatureBuilders :: Assertion-testSignatureBuilders = do- (_, signingKey) <- loadUnencryptedRsaSigner- -- Test RSA builder API with empty subpackets- let builderRsa = sigBuilderInit @'PKA.RSA BinarySig SHA512- let builderRsaWithHashed = addHashedSubs (listToHashedSubs []) builderRsa- let builderRsaFinal = addUnhashedSubs (listToUnhashedSubs []) builderRsaWithHashed- case signDataWithRSABuilder builderRsaFinal signingKey "test payload" of- Left err -> assertFailure ("RSA builder API failed: " ++ renderSignError err)- Right (SigV4 BinarySig RSA SHA512 _ _ _ _) -> pure ()- Right other ->- assertFailure ("RSA builder API should generate BinarySig RSA SHA512, got " ++ show other)-- -- Test RSA builder API with non-default hash algorithm- let builderRsaSha256 = sigBuilderInit @'PKA.RSA BinarySig SHA256- builderRsaSha256WithHashed =- addHashedSubs (listToHashedSubs []) builderRsaSha256- builderRsaSha256Final =- addUnhashedSubs (listToUnhashedSubs []) builderRsaSha256WithHashed- case signDataWithRSABuilder builderRsaSha256Final signingKey "test payload" of- Left err ->- assertFailure ("RSA SHA256 builder API failed: " ++ renderSignError err)- Right (SigV4 BinarySig RSA SHA256 _ _ _ _) ->- pure ()- Right other ->- assertFailure ("RSA builder API should generate BinarySig RSA SHA256, got " ++ show other)-- -- Unsupported hash algorithms should be rejected by RSA PKCS#1 v1.5 backend- let builderRsaUnsupported = sigBuilderInit @'PKA.RSA BinarySig (OtherHA 99)- builderRsaUnsupportedWithHashed =- addHashedSubs (listToHashedSubs []) builderRsaUnsupported- builderRsaUnsupportedFinal =- addUnhashedSubs (listToUnhashedSubs []) builderRsaUnsupportedWithHashed- case signDataWithRSABuilder builderRsaUnsupportedFinal signingKey "test payload" of- Left (SignBackendError _) -> pure ()- Left err ->- assertFailure- ("RSA builder API should reject unsupported hash with backend error, got " ++- renderSignError err)- Right sig ->- assertFailure- ("RSA builder API should reject unsupported hash algorithm, got " ++ show sig)-- -- Runtime hash witness promotion should build RFC9580-compatible builders.- let runtimeRsa =- case sigBuilderInitRuntime @'PKA.RSA RFC9580 BinarySig SHA512 of- Left err -> Left err- Right builder ->- let withHashed = addHashedSubs (listToHashedSubs []) builder- withUnhashed = addUnhashedSubs (listToUnhashedSubs []) withHashed- in first renderSignError (signDataWithRSABuilder withUnhashed signingKey "test payload")- case runtimeRsa of- Left err ->- assertFailure- ("runtime RFC9580 SHA512 builder initialization should succeed, got " ++ err)- Right (SigV4 BinarySig RSA SHA512 _ _ _ _) -> pure ()- Right other ->- assertFailure- ("runtime RFC9580 SHA512 builder should produce BinarySig RSA SHA512, got " ++- show other)-- case sigBuilderInitRuntime @'PKA.RSA RFC9580 BinarySig SHA1 of- Left _ -> pure ()- Right _ ->- assertFailure- "runtime RFC9580 builder initialization should reject deprecated SHA1"-- case sigBuilderInitRuntime @'PKA.RSA RFC9580 BinarySig (OtherHA 99) of- Left _ -> pure ()- Right _ ->- assertFailure- "runtime builder initialization should reject non-witness-backed hash algorithms"- - -- Test Ed25519 builder API with empty subpackets- (_, edSigningKey) <- loadDeterministicEd25519Signer- let builderEd25519 = sigBuilderInit @'PKA.Ed25519 BinarySig SHA512- let builderEd25519WithHashed = addHashedSubs (listToHashedSubs []) builderEd25519- let builderEd25519Final = addUnhashedSubs (listToUnhashedSubs []) builderEd25519WithHashed- case signDataWithEd25519Builder builderEd25519Final edSigningKey "test payload" of- Left err -> assertFailure ("Ed25519 builder API failed: " ++ renderSignError err)- Right (SigV4 BinarySig PKA.Ed25519 SHA512 _ _ _ _) -> pure ()- Right other ->- assertFailure ("Ed25519 builder API should generate BinarySig Ed25519 SHA512, got " ++ show other)- - -- Test Ed25519 v6 builder API- let v6Salt = SignatureSalt (BL.replicate 32 0x42)- let builderEd25519V6 = sigBuilderInitV6 @'PKA.Ed25519 BinarySig SHA512 v6Salt- let builderEd25519V6WithHashed = addHashedSubs (listToHashedSubs []) builderEd25519V6- let builderEd25519V6Final = addUnhashedSubs (listToUnhashedSubs []) builderEd25519V6WithHashed- case signDataWithEd25519V6Builder builderEd25519V6Final edSigningKey "test payload" of- Left err -> assertFailure ("Ed25519 v6 builder API failed: " ++ renderSignError err)- Right (SigV6 BinarySig PKA.Ed25519 SHA512 salt _ _ _ _) ->- assertEqual "Ed25519 v6 builder API should preserve 32-byte salt" 32 (BL.length (unSignatureSalt salt))- Right other ->- assertFailure ("Ed25519 v6 builder API should generate BinarySig Ed25519 SHA512 SigV6, got " ++ show other)-- -- Runtime v6 builder initialization should support SHA3 witness-backed hashes.- let v6Sha3Salt = SignatureSalt (BL.replicate 16 0x33)- runtimeEd25519V6Sha3 =- case sigBuilderInitV6Runtime @'PKA.Ed25519 RFC9580 BinarySig SHA3_256 v6Sha3Salt of- Left err -> Left err- Right builder ->- let withHashed = addHashedSubs (listToHashedSubs []) builder- withUnhashed = addUnhashedSubs (listToUnhashedSubs []) withHashed- in first- renderSignError- (signDataWithEd25519V6Builder withUnhashed edSigningKey "test payload")- case runtimeEd25519V6Sha3 of- Left err ->- assertFailure- ("runtime RFC9580 SHA3-256 v6 builder should succeed, got " ++ err)- Right (SigV6 BinarySig PKA.Ed25519 SHA3_256 salt _ _ _ _) ->- assertEqual- "runtime RFC9580 SHA3-256 v6 builder should preserve 16-byte salt"- 16- (BL.length (unSignatureSalt salt))- Right other ->- assertFailure- ("runtime RFC9580 SHA3-256 v6 builder should produce Ed25519 SigV6 SHA3_256, got " ++- show other)--testLegalSubpacketBuilders :: Assertion-testLegalSubpacketBuilders = do- (signer, signingKey) <- loadUnencryptedRsaSigner- issuerKeyId <-- case eightOctetKeyID signer of- Left err ->- assertFailure ("failed to derive issuer key id for legal-subpacket builder test: " ++ err) >>- fail "expected issuer key id"- Right i -> pure i-- let signerFp = fingerprint signer- creation = ThirtyTwoBitTimeStamp 1- v4Builder = sigBuilderInit @'PKA.RSA BinarySig SHA512- v4WithHashed =- addHashedSubs- (listToLegalSubs [LegalSigCreationTime creation, LegalIssuerFingerprintV4 signerFp])- v4Builder- v4Final = addUnhashedSubs (listToLegalSubs [LegalIssuerV4 issuerKeyId]) v4WithHashed-- case signDataWithRSABuilder v4Final signingKey "typed legal subpacket payload" of- Left err ->- assertFailure ("v4 legal-subpacket builder failed: " ++ renderSignError err)- Right (SigV4 _ _ _ hashed unhashed _ _) -> do- assertBool- "v4 legal-subpacket builder should include creation time in hashed subpackets"- (SigSubPacket False (SigCreationTime creation) `elem` hashed)- assertBool- "v4 legal-subpacket builder should include v4 issuer fingerprint in hashed subpackets"- (SigSubPacket False (IssuerFingerprint IssuerFingerprintV4 signerFp) `elem` hashed)- assertBool- "v4 legal-subpacket builder should include issuer key ID only in unhashed subpackets"- (SigSubPacket False (Issuer issuerKeyId) `elem` unhashed)- Right other ->- assertFailure ("v4 legal-subpacket builder should generate SigV4, got " ++ show other)-- (_, edSigningKey) <- loadDeterministicEd25519Signer- let v6Salt = SignatureSalt (BL.replicate 32 0x17)- v6Builder = sigBuilderInitV6 @'PKA.Ed25519 BinarySig SHA512 v6Salt- v6WithHashed =- addHashedSubs- (listToLegalSubs [LegalSigCreationTime creation, LegalIssuerFingerprintV6 signerFp])- v6Builder- v6Final = addUnhashedSubs (listToLegalSubs []) v6WithHashed-- case signDataWithEd25519V6Builder v6Final edSigningKey "typed legal subpacket payload" of- Left err ->- assertFailure ("v6 legal-subpacket builder failed: " ++ renderSignError err)- Right (SigV6 _ _ _ _ hashed unhashed _ _) -> do- assertBool- "v6 legal-subpacket builder should include creation time in hashed subpackets"- (SigSubPacket False (SigCreationTime creation) `elem` hashed)- assertBool- "v6 legal-subpacket builder should include v6 issuer fingerprint in hashed subpackets"- (SigSubPacket False (IssuerFingerprint IssuerFingerprintV6 signerFp) `elem` hashed)- assertEqual- "v6 legal-subpacket builder should keep unhashed set empty for typed legal set used here"- []- unhashed- Right other ->- assertFailure ("v6 legal-subpacket builder should generate SigV6, got " ++ show other)--testTemporaryKeyValidityRespectsLatestSelfSignature :: Assertion-testTemporaryKeyValidityRespectsLatestSelfSignature = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let uidText = "temporary-validity@example.org"- uid = UserId uidText- keyCreated = _timestamp signer- longValidityCertTime = addTimestampSeconds keyCreated 10- temporaryValidityCertTime = addTimestampSeconds keyCreated 20- validCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 25)- expiredCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 200)- preExpirySignatureTime = addTimestampSeconds keyCreated 25- postExpirySignatureTime = addTimestampSeconds keyCreated 40- payload = "temporary validity signer timeline payload"- longValidityCert <-- signCertificationAt- signer- signingKey- uid- longValidityCertTime- [SigSubPacket False (KeyExpirationTime 1000)]- temporaryValidityCert <-- signCertificationAt- signer- signingKey- uid- temporaryValidityCertTime- [SigSubPacket False (KeyExpirationTime 30)]- preExpirySignature <-- signBinaryMessageWithRSAAt signer signingKey preExpirySignatureTime payload- postExpirySignature <-- signBinaryMessageWithRSAAt signer signingKey postExpirySignatureTime payload- let tk = TKUnknown (signer, Nothing) [] [(uidText, [longValidityCert, temporaryValidityCert])] [] []- keyring = mkTestKeyring [tk]- assertBool- "latest effective self-signature should keep key valid before temporary expiration"- (isTKTimeValid validCheckTime tk)- assertBool- "latest effective self-signature should expire key at the temporary validity boundary"- (not (isTKTimeValid expiredCheckTime tk))- assertSingleSignerFingerprint- "pre-expiry signatures should still verify"- (fingerprint signer)- (verifyTimelinePackets keyring payload preExpirySignature)- assertSingleFailureContainsTimeline- "post-expiry signatures should be rejected"- "signing key was not valid at the signature creation time"- (verifyTimelinePackets keyring payload postExpirySignature)--testTemporaryKeyValidityRejectsSelfSignatureGaps :: Assertion-testTemporaryKeyValidityRejectsSelfSignatureGaps = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let uidText = "temporary-validity-gap@example.org"- uid = UserId uidText- keyCreated = _timestamp signer- firstCertTime = addTimestampSeconds keyCreated 10- secondCertTime = addTimestampSeconds keyCreated 30- gapCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 25)- renewedCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 35)- gapSignatureTime = addTimestampSeconds keyCreated 25- renewedSignatureTime = addTimestampSeconds keyCreated 35- payload = "temporary validity gap signer timeline payload"- firstCertification <-- signCertificationAt- signer- signingKey- uid- firstCertTime- [ SigSubPacket False (KeyExpirationTime 1000)- , SigSubPacket False (SigExpirationTime 10)- ]- secondCertification <-- signCertificationAt- signer- signingKey- uid- secondCertTime- [SigSubPacket False (KeyExpirationTime 1000)]- gapSignature <-- signBinaryMessageWithRSAAt signer signingKey gapSignatureTime payload- renewedSignature <-- signBinaryMessageWithRSAAt signer signingKey renewedSignatureTime payload- let tk = TKUnknown (signer, Nothing) [] [(uidText, [firstCertification, secondCertification])] [] []- keyring = mkTestKeyring [tk]- assertBool- "a gap with no effective self-signature should make the key temporarily invalid"- (not (isTKTimeValid gapCheckTime tk))- assertBool- "a later self-signature should restore validity once it becomes effective"- (isTKTimeValid renewedCheckTime tk)- assertSingleFailureContainsTimeline- "signatures made during a self-signature gap should be rejected"- "signing key was not valid at the signature creation time"- (verifyTimelinePackets keyring payload gapSignature)- assertSingleSignerFingerprint- "signatures made after a later self-signature becomes effective should verify"- (fingerprint signer)- (verifyTimelinePackets keyring payload renewedSignature)--testTemporaryKeyValidityRejectsPreCertificationGap :: Assertion-testTemporaryKeyValidityRejectsPreCertificationGap = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let uidText = "temporary-validity-pre-cert-gap@example.org"- uid = UserId uidText- keyCreated = _timestamp signer- certificationTime = addTimestampSeconds keyCreated 20- invalidCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 10)- validCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 25)- invalidSignatureTime = addTimestampSeconds keyCreated 10- validSignatureTime = addTimestampSeconds keyCreated 25- payload = "temporary validity pre-certification gap signer timeline payload"- certification <-- signCertificationAt- signer- signingKey- uid- certificationTime- [SigSubPacket False (KeyExpirationTime 1000)]- invalidSignature <-- signBinaryMessageWithRSAAt signer signingKey invalidSignatureTime payload- validSignature <-- signBinaryMessageWithRSAAt signer signingKey validSignatureTime payload- let tk = TKUnknown (signer, Nothing) [] [(uidText, [certification])] [] []- keyring = mkTestKeyring [tk]- assertBool- "a key should be invalid before its first self-signature becomes effective"- (not (isTKTimeValid invalidCheckTime tk))- assertBool- "a key should become valid once its first self-signature becomes effective"- (isTKTimeValid validCheckTime tk)- assertSingleFailureContainsTimeline- "signatures made before the first self-signature should be rejected"- "signing key was not valid at the signature creation time"- (verifyTimelinePackets keyring payload invalidSignature)- assertSingleSignerFingerprint- "signatures made after the first self-signature becomes effective should verify"- (fingerprint signer)- (verifyTimelinePackets keyring payload validSignature)--testTemporaryKeyValidityFollowsTemporarySelfRevocation :: Assertion-testTemporaryKeyValidityFollowsTemporarySelfRevocation = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let uidText = "temporary-validity-self-revocation@example.org"- uid = UserId uidText- keyCreated = _timestamp signer- certificationTime = addTimestampSeconds keyCreated 10- revocationTime = addTimestampSeconds keyCreated 20- revokedCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 25)- restoredCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 35)- revokedSignatureTime = addTimestampSeconds keyCreated 25- restoredSignatureTime = addTimestampSeconds keyCreated 35- payload = "temporary validity self-revocation signer timeline payload"- certification <-- signCertificationAt- signer- signingKey- uid- certificationTime- [SigSubPacket False (KeyExpirationTime 1000)]- temporaryRevocation <-- signCertificationRevocationAt- signer- signingKey- uid- revocationTime- [SigSubPacket False (SigExpirationTime 10)]- revokedSignature <-- signBinaryMessageWithRSAAt signer signingKey revokedSignatureTime payload- restoredSignature <-- signBinaryMessageWithRSAAt signer signingKey restoredSignatureTime payload- let tk = TKUnknown (signer, Nothing) [] [(uidText, [certification, temporaryRevocation])] [] []- keyring = mkTestKeyring [tk]- assertBool- "a temporary self-revocation should disable signing while it is effective"- (not (isTKTimeValid revokedCheckTime tk))- assertBool- "a self-certification should become valid again once its temporary revocation expires"- (isTKTimeValid restoredCheckTime tk)- assertSingleFailureContainsTimeline- "signatures made during a temporary self-revocation should be rejected"- "signing key was not valid at the signature creation time"- (verifyTimelinePackets keyring payload revokedSignature)- assertSingleSignerFingerprint- "signatures made after a temporary self-revocation expires should verify"- (fingerprint signer)- (verifyTimelinePackets keyring payload restoredSignature)--testTemporaryKeyValidityDoesNotReviveOlderSelfSignatures :: Assertion-testTemporaryKeyValidityDoesNotReviveOlderSelfSignatures = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let uidText = "temporary-validity-no-revival@example.org"- uid = UserId uidText- keyCreated = _timestamp signer- firstCertTime = addTimestampSeconds keyCreated 10- secondCertTime = addTimestampSeconds keyCreated 20- invalidCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 30)- invalidSignatureTime = addTimestampSeconds keyCreated 30- payload = "temporary validity no revival signer timeline payload"- firstCertification <-- signCertificationAt- signer- signingKey- uid- firstCertTime- [SigSubPacket False (KeyExpirationTime 1000)]- secondCertification <-- signCertificationAt- signer- signingKey- uid- secondCertTime- [ SigSubPacket False (KeyExpirationTime 1000)- , SigSubPacket False (SigExpirationTime 5)- ]- invalidSignature <-- signBinaryMessageWithRSAAt signer signingKey invalidSignatureTime payload- let tk = TKUnknown (signer, Nothing) [] [(uidText, [firstCertification, secondCertification])] [] []- keyring = mkTestKeyring [tk]- assertBool- "an expired newer self-signature should not revive an older certification"- (not (isTKTimeValid invalidCheckTime tk))- assertSingleFailureContainsTimeline- "signatures after a newer self-signature expires should be rejected until re-certified"- "signing key was not valid at the signature creation time"- (verifyTimelinePackets keyring payload invalidSignature)--testUidRetiredRevocationIsHistorical :: Assertion-testUidRetiredRevocationIsHistorical = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let uidText = "retired-uid@example.org"- uid = UserId uidText- keyCreated = _timestamp signer- certificationTime = addTimestampSeconds keyCreated 10- revocationTime = addTimestampSeconds keyCreated 20- certification <-- signCertificationAt signer signingKey uid certificationTime []- retirementRevocation <-- signCertificationRevocationAt- signer- signingKey- uid- revocationTime- [SigSubPacket False (ReasonForRevocation UserIdInfoNoLongerValid "")]- let tk = TKUnknown (signer, Nothing) [] [(uidText, [certification, retirementRevocation])] [] []- verifyAt ts =- verifyUnknownTKWith- (verifySigWith (verifyAgainstKeys [tk]))- (Just (timestampToUTCTime ts))- tk- beforeRetirement <-- case verifyAt (addTimestampSeconds keyCreated 15) of- Left err ->- assertFailure- ("uid-retired pre-revocation verification failed: " ++ renderVerificationError err) >>- fail "expected verified transferable key before uid retirement"- Right verifiedTK -> pure verifiedTK- afterRetirement <-- case verifyAt (addTimestampSeconds keyCreated 25) of- Left err ->- assertFailure- ("uid-retired post-revocation verification failed: " ++ renderVerificationError err) >>- fail "expected verified transferable key after uid retirement"- Right verifiedTK -> pure verifiedTK- assertEqual- "uid-retired revocation should preserve the UID before it becomes effective"- 1- (length (_tkuUIDs beforeRetirement))- assertEqual- "uid-retired revocation should retire the UID once effective"- 0- (length (_tkuUIDs afterRetirement))--testThirdPartyCertificationDoesNotAffectKeyValidityWindow :: Assertion-testThirdPartyCertificationDoesNotAffectKeyValidityWindow = do- (targetSigner, _) <- loadUnencryptedRsaSigner- (thirdPartySigner, thirdPartySigningKey) <- loadDeterministicEd25519Signer- let uidText = "third-party-validity@example.org"- uid = UserId uidText- keyCreated = _timestamp targetSigner- certificationTime = addTimestampSeconds keyCreated 10- validityCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 200)- thirdPartyCertification <-- signCertificationWithEd25519At- targetSigner- thirdPartySigner- thirdPartySigningKey- uid- certificationTime- [SigSubPacket False (KeyExpirationTime 30)]- let targetTK = TKUnknown (targetSigner, Nothing) [] [(uidText, [thirdPartyCertification])] [] []- certifierTK = TKUnknown (thirdPartySigner, Nothing) [] [] [] []- verifyAt =- verifyUnknownTKWith- (verifySigWith (verifyAgainstKeys [targetTK, certifierTK]))- (Just validityCheckTime)- targetTK- verifiedTK <-- case verifyAt of- Left err ->- assertFailure- ("third-party certification verification failed: " ++ renderVerificationError err) >>- fail "expected verified transferable key"- Right tk -> pure tk- assertBool- "third-party certifications must not define the primary key validity window"- (isTKTimeValid validityCheckTime verifiedTK)--testCertificationRevocationOnlyRemovesMatchingSigner :: Assertion-testCertificationRevocationOnlyRemovesMatchingSigner = do- (targetSigner, targetSigningKey) <- loadUnencryptedRsaSigner- (thirdPartySigner, thirdPartySigningKey) <- loadDeterministicEd25519Signer- let uidText = "cert-revocation-scope@example.org"- uid = UserId uidText- keyCreated = _timestamp targetSigner- selfCertificationTime = addTimestampSeconds keyCreated 10- thirdPartyCertificationTime = addTimestampSeconds keyCreated 12- thirdPartyRevocationTime = addTimestampSeconds keyCreated 20- beforeRevocationTime = timestampToUTCTime (addTimestampSeconds keyCreated 15)- afterRevocationTime = timestampToUTCTime (addTimestampSeconds keyCreated 25)- selfCertification <-- signCertificationAt targetSigner targetSigningKey uid selfCertificationTime []- thirdPartyCertification <-- signCertificationWithEd25519At- targetSigner- thirdPartySigner- thirdPartySigningKey- uid- thirdPartyCertificationTime- []- thirdPartyRevocation <-- signCertificationRevocationWithEd25519At- targetSigner- thirdPartySigner- thirdPartySigningKey- uid- thirdPartyRevocationTime- []- let targetTK =- TKUnknown- (targetSigner, Nothing)- []- [(uidText, [selfCertification, thirdPartyCertification, thirdPartyRevocation])]- []- []- certifierTK = TKUnknown (thirdPartySigner, Nothing) [] [] [] []- verifyAt ts =- verifyUnknownTKWith- (verifySigWith (verifyAgainstKeys [targetTK, certifierTK]))- (Just ts)- targetTK- beforeRevocation <-- case verifyAt beforeRevocationTime of- Left err ->- assertFailure- ("pre-revocation certification verification failed: " ++ renderVerificationError err) >>- fail "expected verified transferable key before certification revocation"- Right tk -> pure tk- afterRevocation <-- case verifyAt afterRevocationTime of- Left err ->- assertFailure- ("post-revocation certification verification failed: " ++ renderVerificationError err) >>- fail "expected verified transferable key after certification revocation"- Right tk -> pure tk- let beforeSigs = maybe [] snd (find ((== uidText) . fst) (_tkuUIDs beforeRevocation))- afterSigs = maybe [] snd (find ((== uidText) . fst) (_tkuUIDs afterRevocation))- assertEqual- "certification revocations should not apply before they become effective"- [selfCertification, thirdPartyCertification]- beforeSigs- assertEqual- "a certification revocation should remove only certifications from the same signer"- [selfCertification]- afterSigs--testPreferencesAtTimestamp :: Assertion-testPreferencesAtTimestamp = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let uid = UserId "prefs@example.org"- UserId uidText = uid- t1 = addTimestampSeconds (_timestamp signer) 1- t2 = addTimestampSeconds t1 100- t3 = addTimestampSeconds t2 100- queryDuring = addTimestampSeconds t2 1- queryAfterRevocation = addTimestampSeconds t3 1-- initialCertification <-- signCertificationAt- signer- signingKey- uid- t1- [SigSubPacket False (PreferredHashAlgorithms [SHA256])]- updatedCertification <-- signCertificationAt- signer- signingKey- uid- t2- [SigSubPacket False (PreferredHashAlgorithms [SHA512])]- certificationRevocation <-- signCertificationRevocationAt signer signingKey uid t3 []-- let tk =- TKUnknown- (signer, Nothing)- []- [(uidText, [initialCertification, updatedCertification, certificationRevocation])]- []- []-- assertEqual- "key preferences at timestamp should use the latest active self-certification"- (Just [PreferredHashAlgorithms [SHA512]])- (effectiveKeyPreferencesAtTimestamp queryDuring tk)- assertEqual- "UID preferences at timestamp should use the latest active self-certification"- (Just [PreferredHashAlgorithms [SHA512]])- (effectiveUIDPreferencesAtTimestamp queryDuring uidText tk)- assertEqual- "revoked UID certifications should not provide effective UID preferences"- Nothing- (effectiveUIDPreferencesAtTimestamp queryAfterRevocation uidText tk)- assertEqual- "key preferences should become unavailable when no active self-certification remains"- Nothing- (effectiveKeyPreferencesAtTimestamp queryAfterRevocation tk)--testChangePrivateKeyPassphraseLegacy :: Assertion-testChangePrivateKeyPassphraseLegacy = do- passphrase <- readPKIPassphrase- packets <-- DC.runConduitRes $- CB.sourceFile "tests/data/aes256-sha512.seckey" DC..| conduitGet get DC..|- CL.consume- (pkp, ska) <-- case packets of- (SecretKeyPkt pkpayload skaddendum:_) -> pure (pkpayload, skaddendum)- _ ->- assertFailure "aes256-sha512.seckey did not begin with a secret key packet" >>- fail "expected secret key packet"- originalDecrypted <-- case decryptPrivateKey (pkp, ska) passphrase of- Left err ->- assertFailure ("decrypting original legacy key failed: " ++ err) >>- fail "decrypting original legacy key failed"- Right x -> pure x- originalSKey <-- case originalDecrypted of- SUUnencrypted skey _ -> pure skey- _ ->- assertFailure "original legacy key should decrypt to unencrypted secret material" >>- fail "expected unencrypted secret key"- changed <-- case- changePrivateKeyPassphrase- (pkp, ska)- passphrase- (Salt "12345678")- (IV "1234567890ABCDEF")- "changed-pki-password" of- Left err ->- assertFailure- ("legacy passphrase change should preserve the existing protection envelope, got: " ++- err) >>- fail "legacy passphrase change failed"- Right skaddendum -> pure skaddendum- originalIterCount <-- case ska of- SUSSHA1 AES256 (IteratedSalted SHA512 _ iter) _ _ -> pure iter- _ ->- assertFailure- "legacy key fixture should use SUSSHA1/AES256/IteratedSalted SHA512" >>- fail "expected legacy key fixture"- case changed of- SUSSHA1 AES256 (IteratedSalted SHA512 _ iter) _ encryptedPayload -> do- assertEqual- "legacy passphrase change should preserve the S2K iteration count"- originalIterCount- iter- assertBool- "legacy passphrase change should emit encrypted payload"- (not (BL.null encryptedPayload))- _ ->- assertFailure- "legacy passphrase change should preserve the SUSSHA1/AES256 envelope"- decrypted <-- case decryptPrivateKey (pkp, changed) "changed-pki-password" of- Left err ->- assertFailure ("re-decrypting changed legacy key failed: " ++ err) >>- fail "re-decrypting changed legacy key failed"- Right x -> pure x- case decrypted of- SUUnencrypted skey _ ->- assertEqual- "legacy passphrase change should preserve secret key material"- originalSKey- skey- _ ->- assertFailure- "legacy passphrase change should decrypt back to unencrypted secret material"--testChangePrivateKeyPassphraseV6 :: Assertion-testChangePrivateKeyPassphraseV6 = do- oldPassphrase <- readPKIPassphrase- armors <- loadArmor "v6-encrypted-secret.pgp.aa"- armor <-- case armors of- (a:_) -> pure a- [] ->- assertFailure "v6-encrypted-secret.pgp.aa should contain one armored payload" >>- fail "expected one armored payload"- let packets = parsePkts (armorPayload armor)- (pkp, ska) <-- case packets of- (SecretKeyPkt pkpayload skaddendum:_) -> pure (pkpayload, skaddendum)- _ ->- assertFailure "v6-encrypted-secret.pgp.aa did not begin with a secret key packet" >>- fail "expected secret key packet"- let newPassphrase = "changed-pki-password"- originalDecrypted <-- case decryptPrivateKey (pkp, ska) oldPassphrase of- Left err ->- assertFailure ("decrypting original v6 key failed: " ++ err) >>- fail "decrypting original v6 key failed"- Right x -> pure x- originalSKey <-- case originalDecrypted of- SUUnencrypted skey _ -> pure skey- _ ->- assertFailure "original v6 key should decrypt to unencrypted secret material" >>- fail "expected unencrypted secret key"- let changedResult =- changePrivateKeyPassphrase- (pkp, ska)- oldPassphrase- (Salt "1234567890ABCDEF")- (IV "1234567890ABCDE")- newPassphrase- changed <-- case changedResult of- Left err ->- assertFailure ("changing v6 key passphrase failed: " ++ err) >> pure ska- Right skaddendum -> pure skaddendum- case changed of- SUSAEAD AES256 OCB (Argon2 _ t p em) _ encryptedPayload -> do- assertEqual- "changed v6 key addendum should use expected Argon2 t parameter"- 1- t- assertEqual- "changed v6 key addendum should use expected Argon2 p parameter"- 4- p- assertEqual- "changed v6 key addendum should use expected Argon2 encoded memory parameter"- 15- em- assertBool- "changed v6 key addendum should contain encrypted key material"- (not (BL.null encryptedPayload))- _ -> assertFailure "changed v6 key addendum should be encrypted with SUSAEAD"- let decryptedResult = decryptPrivateKey (pkp, changed) newPassphrase- decrypted <-- case decryptedResult of- Left err ->- assertFailure ("decryption with changed v6 passphrase failed: " ++ err) >>- fail "decryption with changed v6 passphrase failed"- Right x -> pure x- case decrypted of- SUUnencrypted skey _ ->- assertEqual- "changing a v6 key passphrase should preserve secret key material"- originalSKey- skey- _ -> assertFailure "changed v6 key should decrypt with the new passphrase"--testPolicySignatureContextValidation :: Assertion-testPolicySignatureContextValidation = do- let pkSigV4PK = SigV4 KeyRevocationSig RSA SHA512 [] [] 0 (MPI 0 :| [])- pkSigV4Direct = SigV4 SignatureDirectlyOnAKey RSA SHA512 [] [] 0 (MPI 0 :| [])- pkSigV4Binding = SigV4 SubkeyBindingSig RSA SHA512 [] [] 0 (MPI 0 :| [])- pkSigV6PK =- SigV6- KeyRevocationSig- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x11))- []- []- 0- (MPI 0 :| [])- pkSigV6Direct =- SigV6- SignatureDirectlyOnAKey- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x12))- []- []- 0- (MPI 0 :| [])- pkSigV6Binding =- SigV6- SubkeyBindingSig- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x13))- []- []- 0- (MPI 0 :| [])- skSigV4Binding = SigV4 SubkeyBindingSig RSA SHA512 [] [] 0 (MPI 0 :| [])- skSigV4Revocation = SigV4 SubkeyRevocationSig RSA SHA512 [] [] 0 (MPI 0 :| [])- skSigV4Direct = SigV4 SignatureDirectlyOnAKey RSA SHA512 [] [] 0 (MPI 0 :| [])- skSigV6Binding =- SigV6- SubkeyBindingSig- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x14))- []- []- 0- (MPI 0 :| [])- skSigV6Revocation =- SigV6- SubkeyRevocationSig- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x15))- []- []- 0- (MPI 0 :| [])- skSigV6Direct =- SigV6- SignatureDirectlyOnAKey- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x16))- []- []- 0- (MPI 0 :| [])- uidSigV4Generic = SigV4 GenericCert RSA SHA512 [] [] 0 (MPI 0 :| [])- uidSigV4Persona = SigV4 PersonaCert RSA SHA512 [] [] 0 (MPI 0 :| [])- uidSigV4Casual = SigV4 CasualCert RSA SHA512 [] [] 0 (MPI 0 :| [])- uidSigV4Positive = SigV4 PositiveCert RSA SHA512 [] [] 0 (MPI 0 :| [])- uidSigV4CertRev = SigV4 CertRevocationSig RSA SHA512 [] [] 0 (MPI 0 :| [])- uidSigV4Binding = SigV4 SubkeyBindingSig RSA SHA512 [] [] 0 (MPI 0 :| [])- uidSigV6Generic =- SigV6- GenericCert- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x17))- []- []- 0- (MPI 0 :| [])- uidSigV6Persona =- SigV6- PersonaCert- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x18))- []- []- 0- (MPI 0 :| [])- uidSigV6Casual =- SigV6- CasualCert- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x19))- []- []- 0- (MPI 0 :| [])- uidSigV6Positive =- SigV6- PositiveCert- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x1a))- []- []- 0- (MPI 0 :| [])- uidSigV6CertRev =- SigV6- CertRevocationSig- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x1b))- []- []- 0- (MPI 0 :| [])- uidSigV6Binding =- SigV6- SubkeyBindingSig- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x1c))- []- []- 0- (MPI 0 :| [])- assertTrue "KeyRevocationSig allowed on primary key" (isAllowedPrimaryKeySig pkSigV4PK)- assertTrue "SignatureDirectlyOnAKey allowed on primary key" (isAllowedPrimaryKeySig pkSigV4Direct)- assertFalse "SubkeyBindingSig not allowed on primary key" (isAllowedPrimaryKeySig pkSigV4Binding)- assertTrue "v6 KeyRevocationSig allowed on primary key" (isAllowedPrimaryKeySig pkSigV6PK)- assertTrue "v6 SignatureDirectlyOnAKey allowed on primary key" (isAllowedPrimaryKeySig pkSigV6Direct)- assertFalse "v6 SubkeyBindingSig not allowed on primary key" (isAllowedPrimaryKeySig pkSigV6Binding)- assertTrue "SubkeyBindingSig allowed on subkey" (isAllowedSubkeySig skSigV4Binding)- assertTrue "SubkeyRevocationSig allowed on subkey" (isAllowedSubkeySig skSigV4Revocation)- assertFalse "SignatureDirectlyOnAKey not allowed on subkey" (isAllowedSubkeySig skSigV4Direct)- assertTrue "v6 SubkeyBindingSig allowed on subkey" (isAllowedSubkeySig skSigV6Binding)- assertTrue "v6 SubkeyRevocationSig allowed on subkey" (isAllowedSubkeySig skSigV6Revocation)- assertFalse "v6 SignatureDirectlyOnAKey not allowed on subkey" (isAllowedSubkeySig skSigV6Direct)- assertTrue "GenericCert allowed on UID" (isAllowedUIDSig uidSigV4Generic)- assertTrue "PersonaCert allowed on UID" (isAllowedUIDSig uidSigV4Persona)- assertTrue "CasualCert allowed on UID" (isAllowedUIDSig uidSigV4Casual)- assertTrue "PositiveCert allowed on UID" (isAllowedUIDSig uidSigV4Positive)- assertTrue "CertRevocationSig allowed on UID" (isAllowedUIDSig uidSigV4CertRev)- assertFalse "SubkeyBindingSig not allowed on UID" (isAllowedUIDSig uidSigV4Binding)- assertTrue "v6 GenericCert allowed on UID" (isAllowedUIDSig uidSigV6Generic)- assertTrue "v6 PersonaCert allowed on UID" (isAllowedUIDSig uidSigV6Persona)- assertTrue "v6 CasualCert allowed on UID" (isAllowedUIDSig uidSigV6Casual)- assertTrue "v6 PositiveCert allowed on UID" (isAllowedUIDSig uidSigV6Positive)- assertTrue "v6 CertRevocationSig allowed on UID" (isAllowedUIDSig uidSigV6CertRev)- assertFalse "v6 SubkeyBindingSig not allowed on UID" (isAllowedUIDSig uidSigV6Binding)--testVerifySigWithV6RevocationRejectsLegacyIssuerKeyID :: Assertion-testVerifySigWithV6RevocationRejectsLegacyIssuerKeyID = do- (signer, _) <- loadUnencryptedRsaSigner- issuerKeyId <-- case eightOctetKeyID signer of- Left err ->- assertFailure ("failed to derive RSA issuer key id: " ++ err) >>- fail "expected issuer key id"- Right i -> pure i- let sigPayload =- SigV6- KeyRevocationSig- RSA- SHA256- (SignatureSalt (BL.replicate 16 0))- [SigSubPacket False (Issuer issuerKeyId)]- []- 0- (MPI 0 :| [])- verifyFn _ _ _ = Right (Verification signer sigPayload [])- result =- verifySigWith- verifyFn- (SignaturePkt sigPayload)- emptyPSC {lastPrimaryKey = PublicKeyPkt signer}- Nothing- case result of- Left err ->- assertBool- "v6 revocation verification should reject legacy Issuer key-id subpackets"- ("Issuer Key ID subpacket is prohibited in v6 signatures" `isInfixOf`- renderVerificationError err)- Right _ ->- assertFailure- "Expected v6 revocation verification to reject legacy Issuer key-id subpacket"--testVerifySigWithV6RevocationRejectsLegacyIssuerKeyIDInUnhashed :: Assertion-testVerifySigWithV6RevocationRejectsLegacyIssuerKeyIDInUnhashed = do- (signer, _) <- loadUnencryptedRsaSigner- issuerKeyId <-- case eightOctetKeyID signer of- Left err ->- assertFailure ("failed to derive RSA issuer key id: " ++ err) >>- fail "expected issuer key id"- Right i -> pure i- let sigPayload =- SigV6- KeyRevocationSig- RSA- SHA256- (SignatureSalt (BL.replicate 16 0))- []- [SigSubPacket False (Issuer issuerKeyId)]- 0- (MPI 0 :| [])- verifyFn _ _ _ = Right (Verification signer sigPayload [])- result =- verifySigWith- verifyFn- (SignaturePkt sigPayload)- emptyPSC {lastPrimaryKey = PublicKeyPkt signer}- Nothing- case result of- Left err ->- assertBool- "v6 revocation verification should reject legacy Issuer key-id subpackets in unhashed area"- ("Issuer Key ID subpacket is prohibited in v6 signatures" `isInfixOf`- renderVerificationError err)- Right _ ->- assertFailure- "Expected v6 revocation verification to reject legacy Issuer key-id subpacket in unhashed area"--testVerifySigWithV6RevocationAcceptsMatchingIssuerFingerprint :: Assertion-testVerifySigWithV6RevocationAcceptsMatchingIssuerFingerprint = do- (signer, _) <- loadUnencryptedRsaSigner- let sigPayload =- SigV6- KeyRevocationSig- RSA- SHA256- (SignatureSalt (BL.replicate 16 0))- [SigSubPacket False (IssuerFingerprint IssuerFingerprintV6 (fingerprint signer))]- []- 0- (MPI 0 :| [])- verifyFn _ _ _ = Right (Verification signer sigPayload [])- result =- verifySigWith- verifyFn- (SignaturePkt sigPayload)- emptyPSC {lastPrimaryKey = PublicKeyPkt signer}- Nothing- case result of- Left err ->- assertFailure- ("Expected matching IssuerFingerprint to verify for v6 revocation signature, got: " ++- renderVerificationError err)- Right _ -> pure ()--testV6SignerConstructorRejectsV4Key :: Assertion-testV6SignerConstructorRejectsV4Key = do- (signer, signingKey) <- loadDeterministicEd25519Signer- case asV6PKPayload signer of- Left _ -> pure ()- Right signerV6 ->- seq (mkEd25519SignerV6 signerV6 signingKey) $- assertFailure "asV6PKPayload should reject v4 Ed25519 key payloads"- (rsaSigner, rsaSigningKey) <- loadUnencryptedRsaSigner- case asV6PKPayload rsaSigner of- Left _ -> pure ()- Right signerV6 ->- seq (mkRSASignerV6 signerV6 rsaSigningKey) $- assertFailure "asV6PKPayload should reject v4 RSA key payloads"--testGetSecretKeyHandlesV4X25519ECDHPubkey :: Assertion-testGetSecretKeyHandlesV4X25519ECDHPubkey = do- let pkp =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- X25519- (ECDHPubKey- (EdDSAPubKey Ed25519 (PrefixedNativeEPoint (EPoint (os2ip (B.cons 0x40 (B.replicate 32 0x01))))))- SHA256- AES128)- encodedSecret = runPut (put (MPI 42))- case runGetOrFail (getSecretKey pkp) encodedSecret of- Left (_, _, err) ->- assertFailure ("getSecretKey should parse v4 X25519 secret key material: " ++ err)- Right (_, _, ECDHPrivateKey (ECDSA_PrivateKey prv)) ->- assertEqual- "getSecretKey should preserve X25519 private scalar"- 42- (ECDSA.private_d prv)- Right (_, _, other) ->- assertFailure ("unexpected parsed secret key type: " ++ show other)--testV4Ed25519SecretKeyRoundTripPreservesLeadingZeroByte :: Assertion-testV4Ed25519SecretKeyRoundTripPreservesLeadingZeroByte = do- let secretBytes = B.pack (0 : replicate 31 1)- pkt =- SecretKeyPkt- (PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- EdDSA- (EdDSAPubKey Ed25519 (PrefixedNativeEPoint (EPoint (os2ip (B.cons 0x40 (B.replicate 32 0x01)))))))- (SUUnencrypted (EdDSAPrivateKey Ed25519 secretBytes) 0)- case runGetOrFail (get :: Get Pkt) (runPut (put pkt)) of- Left (_, _, err) ->- assertFailure ("v4 Ed25519 secret key should round-trip: " ++ err)- Right (_, _, parsedPkt) ->- case parsedPkt of- SecretKeyPkt _ (SUUnencrypted (EdDSAPrivateKey Ed25519 parsedBytes) _) ->- assertEqual- "v4 Ed25519 secret-key round-trip should preserve MPI-encoded bytes"- secretBytes- parsedBytes- _ ->- assertFailure ("unexpected parsed packet shape: " ++ show parsedPkt)--testV4Ed448SecretKeyRoundTripPreservesLeadingZeroByte :: Assertion-testV4Ed448SecretKeyRoundTripPreservesLeadingZeroByte = do- let secretBytes = B.pack (0 : replicate 56 2)- pkp =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- EdDSA- (EdDSAPubKey Ed448 (PrefixedNativeEPoint (EPoint (os2ip (B.cons 0x40 (B.replicate 57 0x02))))))- encodedSecret = runPut (put (MPI (os2ip secretBytes)))- case runGetOrFail (getSecretKey pkp) encodedSecret of- Left (_, _, err) ->- assertFailure ("v4 Ed448 secret key should parse with preserved padding: " ++ err)- Right (_, _, parsedSecret) ->- assertEqual- "v4 Ed448 secret-key parsing should restore the curve-width byte string"- (EdDSAPrivateKey Ed448 secretBytes)- parsedSecret--testMkUnencryptedSKAddendumComputesLegacyChecksum :: Assertion-testMkUnencryptedSKAddendumComputesLegacyChecksum = do- packets <-- DC.runConduitRes $- CB.sourceFile "tests/data/unencrypted.seckey" DC..| conduitGet get DC..| CL.consume- case packets of- (SecretKeyPkt pkp (SUUnencrypted sk _):_) ->- case mkUnencryptedSKAddendum pkp sk of- Left err ->- assertFailure ("mkUnencryptedSKAddendum should succeed for fixture key: " ++ err)- Right (SUUnencrypted _ actualChecksum) -> do- skPayload <-- case putSKeyForPKPayload pkp sk of- Left err ->- assertFailure ("failed to serialize secret key payload for checksum expectation: " ++ err) >>- fail "expected serializable secret key payload"- Right payload -> pure payload- let expectedChecksum =- fromIntegral- (BL.foldl'- (\acc octet -> (acc + fromIntegral octet) `mod` (65536 :: Integer))- 0- (runPut skPayload) :: Integer)- assertEqual- "mkUnencryptedSKAddendum should use OpenPGP 16-bit checksum over serialized key material"- expectedChecksum- actualChecksum- Right other ->- assertFailure ("unexpected addendum constructor: " ++ show other)- _ ->- assertFailure "unencrypted.seckey did not begin with an unencrypted secret key packet"--testMkUnencryptedSKAddendumUsesV6ChecksumConvention :: Assertion-testMkUnencryptedSKAddendumUsesV6ChecksumConvention = do- (pkp, rsaPrivateKey) <- loadUnencryptedRsaSignerV6- case mkUnencryptedSKAddendum pkp (RSAPrivateKey (RSA_PrivateKey rsaPrivateKey)) of- Left err ->- assertFailure ("mkUnencryptedSKAddendum should support v6 key payloads: " ++ err)- Right (SUUnencrypted _ checksum) ->- assertEqual "v6 unencrypted addendum checksum should be zero" 0 checksum- Right other ->- assertFailure ("unexpected addendum constructor: " ++ show other)--testReinterpretUnknownSKeyForPKPayloadDecodesEdDSA :: Assertion-testReinterpretUnknownSKeyForPKPayloadDecodesEdDSA = do- let secretBytes = B.pack (0 : replicate 31 1)- pkp =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- EdDSA- (EdDSAPubKey Ed25519 (PrefixedNativeEPoint (EPoint (os2ip (B.cons 0x40 (B.replicate 32 0x01))))))- unknown = UnknownSKey (runPut (put (MPI (os2ip secretBytes))))- case reinterpretUnknownSKeyForPKPayload pkp unknown of- Left err ->- assertFailure ("reinterpretUnknownSKeyForPKPayload should decode Ed25519 key: " ++ err)- Right skey ->- assertEqual- "reinterpretUnknownSKeyForPKPayload should return typed EdDSA secret key"- (EdDSAPrivateKey Ed25519 secretBytes)- skey--testFromPrimaryKeyPktToTKUnknownRejectsSubkey :: Assertion-testFromPrimaryKeyPktToTKUnknownRejectsSubkey = do- packets <-- DC.runConduitRes $- CB.sourceFile "tests/data/unencrypted.seckey" DC..| conduitGet get DC..| CL.consume- case packets of- (SecretKeyPkt pkp _ : _) ->- case fromPrimaryKeyPktToTKUnknown (PublicSubkeyPkt pkp) of- Left err ->- assertBool- "fromPrimaryKeyPktToTKUnknown should report non-primary packet tags"- ("expected primary key packet" `isInfixOf` err)- Right _ ->- assertFailure "fromPrimaryKeyPktToTKUnknown should reject subkey packets"- _ ->- assertFailure "unencrypted.seckey did not begin with a secret key packet"+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Tests.Keys (keyAndVerificationTests) where++import Control.Error.Util (isRight)+import Crypto.Number.Serialize (os2ip)+import qualified Crypto.PubKey.ECC.ECDSA as ECDSA+import Data.Bifunctor (first)+import Data.Binary (get, put)+import Data.Binary.Get (Get, runGetOrFail)+import Data.Binary.Put (runPut)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import qualified Data.Conduit as DC+import qualified Data.Conduit.Binary as CB+import qualified Data.Conduit.List as CL+import Data.Conduit.Serialization.Binary (conduitGet)+import Data.IxSet.Typed (getOne, size, (@=))+import Data.List (find, isInfixOf)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Maybe (isJust)+import qualified Data.Set as Set+import Data.Time.Clock.POSIX (posixSecondsToUTCTime)+import Prettyprinter (pretty)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit+ ( Assertion+ , assertBool+ , assertEqual+ , assertFailure+ , testCase+ )++import Codec.Encryption.OpenPGP.Expirations+ ( effectiveKeyPreferencesAtTimestamp+ , effectiveUIDPreferencesAtTimestamp+ , isTKTimeValid+ )+import Codec.Encryption.OpenPGP.Fingerprint+ ( eightOctetKeyID+ , fingerprint+ )+import Codec.Encryption.OpenPGP.Internal+ ( emptyPSC+ , lastPrimaryKey+ , lastSubkey+ )+import Codec.Encryption.OpenPGP.KeyInfo+ ( pkalgoAbbrev+ , pubkeySize+ )+import Codec.Encryption.OpenPGP.Message+ ( asV6PKPayload+ , mkEd25519SignerV6+ , mkRSASignerV6+ )+import Codec.Encryption.OpenPGP.Policy+ ( OpenPGPRFC (..)+ , defaultVerificationPolicy+ , isAllowedPrimaryKeySig+ , isAllowedSubkeySig+ , isAllowedUIDSig+ )+import Codec.Encryption.OpenPGP.SecretKey+ ( changePrivateKeyPassphrase+ , decryptPrivateKey+ , mkUnencryptedSKAddendum+ , reinterpretUnknownSKeyForPKPayload+ )+import Codec.Encryption.OpenPGP.Serialize+ ( getSecretKey+ , parsePkts+ , putSKeyForPKPayload+ )+import Codec.Encryption.OpenPGP.SerializeForSigs (payloadForSig)+import Codec.Encryption.OpenPGP.Signatures+import Codec.Encryption.OpenPGP.Subpackets+ ( LegalSubpacket (..)+ , addHashedSubs+ , addUnhashedSubs+ , listToHashedSubs+ , listToLegalSubs+ , listToUnhashedSubs+ , sigBuilderInit+ , sigBuilderInitRuntime+ , sigBuilderInitV6+ , sigBuilderInitV6Runtime+ )+import Codec.Encryption.OpenPGP.Types+import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as PKA+import Data.Conduit.OpenPGP.Compression (conduitDecompress)+import Data.Conduit.OpenPGP.Keyring+ ( conduitDropErrorsAndNothings+ , conduitToTKsEither+ )+import Data.Conduit.OpenPGP.Verify (conduitVerify)+import Tests.Common+ ( addTimestampSeconds+ , armorPayload+ , assertFalse+ , assertSingleFailureContainsTimeline+ , assertSingleSignerFingerprint+ , assertTrue+ , certificateVerificationFixtures+ , fixturePath+ , loadAndDecompressPkts+ , loadArmor+ , loadDeterministicEd25519Signer+ , loadKeyring+ , loadUnencryptedRsaSigner+ , loadUnencryptedRsaSignerV6+ , messageIssuerSubpacketsAt+ , messageVerificationFixtures+ , mkTestKeyring+ , readFixturePayload+ , readPKIPassphrase+ , runGet+ , setKeyVersion+ , signBinaryMessageWithRSAAt+ , signCertificationAt+ , signCertificationRevocationAt+ , signCertificationRevocationWithEd25519At+ , signCertificationWithEd25519At+ , signSubkeyBindingWithRSAExtrasAt+ , timestampToUTCTime+ , verificationFixtureGroup+ , verifyMessageFromBytestring+ , verifyMessageFromBytestringBatch+ , verifyMessageFromPackets+ , verifyMessageFromPacketsBatch+ , verifyTimelinePackets+ )++keyAndVerificationTests :: TestTree+keyAndVerificationTests =+ testGroup+ "Keys and verification"+ [ testGroup+ "PKA/Size/KeyID/fingerprint group"+ [ testCase+ "v3 key"+ ( testPKAandSizeAndKeyIDandFingerprint+ "v3.key"+ "rsa/1024:6DC580F5C7261095/CBD9 F412 6807 E405 CC2D 2712 1DF5 E86E"+ )+ , testCase+ "v4 key"+ ( testPKAandSizeAndKeyIDandFingerprint+ "000001-006.public_key"+ "rsa/1248:D4D54EA16F87040E/421F 28FE AAD2 22F8 56C8 FFD5 D4D5 4EA1 6F87 040E"+ )+ , testCase+ "ECDSA key"+ ( testPKAandSizeAndKeyIDandFingerprint+ "nist_p-256_key.gpg"+ "ecd/256:F7708BADD6063224/174C CF12 C571 6D0E 527F B50E F770 8BAD D606 3224"+ )+ , testCase+ "EdDSA key"+ ( testPKAandSizeAndKeyIDandFingerprint+ "sample-eddsa.pubkey"+ "edd/256:8CFDE12197965A9A/C959 BDBA FA32 A2F8 9A15 3B67 8CFD E121 9796 5A9A"+ )+ , testCase+ "EdDSA secret key (projected to public key packet)"+ ( testPKAandSizeAndKeyIDandFingerprint+ "ed25519.secretkey"+ "edd/256:B05F9287601D5914/B6FE 0C23 12EB D832 0238 C252 B05F 9287 601D 5914"+ )+ , testCase+ "v6 secret key (projected to public key packet)"+ ( testPKAandSizeAndKeyIDandFingerprint+ "v6-secret.pgp.aa"+ "e25/256:7106CB6DB27E4FC9/7106 CB6D B27E 4FC9 B050 B02B B316 4DBD 4BBD 7263 D4D2 9E78 A3BB 3BDB B022 2D19"+ )+ ]+ , testGroup+ "Keyring group"+ [ testCase+ "pubring 7732CF988A63EA86"+ (testKeyringLookup "pubring.gpg" "7732CF988A63EA86" True)+ , testCase+ "pubring 123456789ABCDEF0"+ (testKeyringLookup "pubring.gpg" "123456789ABCDEF0" False)+ , testCase+ "pubsub AD992E9C24399832"+ (testKeyringLookup "pubring.gpg" "AD992E9C24399832" True)+ , testCase+ "secring 7732CF988A63EA86"+ (testKeyringLookup "secring.gpg" "7732CF988A63EA86" True)+ , testCase+ "secring 123456789ABCDEF0"+ (testKeyringLookup "secring.gpg" "123456789ABCDEF0" False)+ , testCase+ "secsub AD992E9C24399832"+ (testKeyringLookup "secring.gpg" "AD992E9C24399832" True)+ , testCase+ "pubring.gpg has 4 keys"+ (testKeyringSize "pubring.gpg" 4)+ , testCase+ "secring.gpg has 4 keys"+ (testKeyringSize "secring.gpg" 4)+ ]+ , verificationFixtureGroup+ "Message verification group (packet-based)"+ verifyMessageFromPackets+ messageVerificationFixtures+ , verificationFixtureGroup+ "Message verification group (packet-based, batch)"+ verifyMessageFromPacketsBatch+ messageVerificationFixtures+ , verificationFixtureGroup+ "Message verification group (bytestring-based)"+ verifyMessageFromBytestring+ messageVerificationFixtures+ , verificationFixtureGroup+ "Message verification group (bytestring-based, batch)"+ verifyMessageFromBytestringBatch+ messageVerificationFixtures+ , verificationFixtureGroup+ "Certificate verification group (packet-based)"+ verifyMessageFromPackets+ certificateVerificationFixtures+ , verificationFixtureGroup+ "Certificate verification group (packet-based, batch)"+ verifyMessageFromPacketsBatch+ certificateVerificationFixtures+ , verificationFixtureGroup+ "Certificate verification group (bytestring-based)"+ verifyMessageFromBytestring+ certificateVerificationFixtures+ , verificationFixtureGroup+ "Certificate verification group (bytestring-based, batch)"+ verifyMessageFromBytestringBatch+ certificateVerificationFixtures+ , testGroup+ "Key verification group"+ [ testCase+ "6F87040E pubkey"+ (testKeysSelfVerification True "6F87040E.pubkey")+ , testCase+ "revoked pubkey"+ (testKeysSelfVerification False "revoked.pubkey")+ , testCase+ "expired pubkey"+ (testKeysSelfVerification True "expired.pubkey")+ , testCase+ "nist_p-256 pubkey"+ (testKeysSelfVerification True "nist_p-256_key.gpg")+ , testCase+ "ed25519 pubkey"+ (testKeysSelfVerification True "ed25519.pubkey")+ ]+ , testGroup+ "Verification error messaging group"+ [ testCase+ "missing signing key in keyring"+ ( testMissingSigningKeyMessage+ "ecdsa-key-without-ecdh.pubkey"+ "uncompressed-ops-rsa.gpg"+ )+ , testCase+ "tampered message signature mismatch"+ ( testTamperedSignatureMessage+ "pubring.gpg"+ "uncompressed-ops-rsa.gpg"+ )+ , testCase+ "revoked certificate reports revocation"+ (testRevokedCertificateMessage "revoked.pubkey")+ ]+ , testGroup+ "Key expiration group"+ [ testCase+ "6F87040E pubkey"+ (testKeysExpiration True "6F87040E.pubkey")+ , testCase+ "expired pubkey"+ (testKeysExpiration False "expired.pubkey")+ , testCase+ "nist_p-256 pubkey"+ (testKeysExpiration True "nist_p-256_key.gpg")+ , testCase+ "ed25519-without-curve25519.pubkey"+ (testKeysExpiration True "ed25519-without-curve25519.pubkey")+ , testCase+ "ed25519.pubkey"+ (testKeysExpiration True "ed25519.pubkey")+ ]+ , testGroup+ "misc group"+ [ testCase+ "conduitVerify processes SigV6 packets"+ testConduitVerifyProcessesSigV6+ , testCase "signature builder API (Phase 2)" testSignatureBuilders+ , testCase+ "typed legal subpacket builder API (P2.1)"+ testLegalSubpacketBuilders+ , testCase+ "legacy secret key passphrase changes preserve protection"+ testChangePrivateKeyPassphraseLegacy+ , testCase+ "change private key passphrase"+ testChangePrivateKeyPassphraseV6+ , testCase+ "getSecretKey parses v4 X25519 ECDH key material"+ testGetSecretKeyHandlesV4X25519ECDHPubkey+ , testCase+ "v4 Ed25519 secret-key roundtrip preserves leading zero byte"+ testV4Ed25519SecretKeyRoundTripPreservesLeadingZeroByte+ , testCase+ "v4 Ed448 secret-key roundtrip preserves leading zero byte"+ testV4Ed448SecretKeyRoundTripPreservesLeadingZeroByte+ , testCase+ "mkUnencryptedSKAddendum computes legacy secret-key checksum"+ testMkUnencryptedSKAddendumComputesLegacyChecksum+ , testCase+ "mkUnencryptedSKAddendum sets v6 unencrypted checksum to zero"+ testMkUnencryptedSKAddendumUsesV6ChecksumConvention+ , testCase+ "reinterpretUnknownSKeyForPKPayload decodes EdDSA unknown key material"+ testReinterpretUnknownSKeyForPKPayloadDecodesEdDSA+ , testCase+ "fromPrimaryKeyPktToTKUnknown rejects subkey packets"+ testFromPrimaryKeyPktToTKUnknownRejectsSubkey+ , testCase+ "policy signature context validation (RFC9580)"+ testPolicySignatureContextValidation+ , testCase+ "temporary validity uses latest effective self-signature window"+ testTemporaryKeyValidityRespectsLatestSelfSignature+ , testCase+ "temporary validity rejects gaps with no effective self-signature"+ testTemporaryKeyValidityRejectsSelfSignatureGaps+ , testCase+ "temporary validity rejects the gap before the first self-signature"+ testTemporaryKeyValidityRejectsPreCertificationGap+ , testCase+ "temporary validity follows temporary self-revocations"+ testTemporaryKeyValidityFollowsTemporarySelfRevocation+ , testCase+ "temporary validity does not revive older self-signatures after newer expiry"+ testTemporaryKeyValidityDoesNotReviveOlderSelfSignatures+ , testCase+ "uid-retired revocation is historical until effective"+ testUidRetiredRevocationIsHistorical+ , testCase+ "third-party certifications do not affect primary key validity windows"+ testThirdPartyCertificationDoesNotAffectKeyValidityWindow+ , testCase+ "certification revocations only remove matching signer certifications"+ testCertificationRevocationOnlyRemovesMatchingSigner+ , testCase+ "preference resolution tracks active self-certification timeline"+ testPreferencesAtTimestamp+ , testCase+ "verifySigWith rejects v6 revocation Issuer key-id subpackets"+ testVerifySigWithV6RevocationRejectsLegacyIssuerKeyID+ , testCase+ "verifySigWith rejects v6 revocation unhashed Issuer key-id subpackets"+ testVerifySigWithV6RevocationRejectsLegacyIssuerKeyIDInUnhashed+ , testCase+ "verifySigWith accepts matching v6 revocation IssuerFingerprint"+ testVerifySigWithV6RevocationAcceptsMatchingIssuerFingerprint+ , testCase+ "v6 signer constructor rejects v4 key"+ testV6SignerConstructorRejectsV4Key+ , testCase+ "primary key binding rejects unsupported critical hashed subpackets"+ testPrimaryKeyBindingRejectsUnsupportedCriticalSubpacket+ , testCase+ "subkey binding missing back-signature is surfaced as a warning"+ testSubkeyBindingMissingBackSignatureProducesWarning+ , testCase+ "subkey binding rejects unsupported critical hashed subpackets"+ testSubkeyBindingRejectsUnsupportedCriticalSubpacket+ ]+ ]++testPKAandSizeAndKeyIDandFingerprint+ :: FilePath -> String -> Assertion+testPKAandSizeAndKeyIDandFingerprint fpr kf = do+ bs <- readFixturePayload fpr+ case runGet (get :: Get Pkt) bs of+ Left _ -> assertFailure $ "Decoding of " ++ fpr ++ " broke."+ Right pkt ->+ case publicKeyPacketOf pkt of+ PublicKeyPkt pkp -> do+ let pref =+ concat+ [ pkalgoAbbrev (_pkalgo pkp)+ , "/"+ , either (const "unknown") show (pubkeySize (_pubkey pkp))+ , ":"+ , either (const "unknown") (show . pretty) (eightOctetKeyID pkp)+ , "/"+ ]+ assertEqual+ ("for " ++ fpr ++ " (spaceless)")+ (spaceless kf)+ (pref ++ show (pretty (fingerprint pkp)))+ assertEqual+ ("for " ++ fpr ++ " (spaced)")+ kf+ (pref ++ show (pretty (SpacedFingerprint (fingerprint pkp))))+ _ ->+ assertFailure+ "Expected (possibly secret) key packet, got something else."+ where+ spaceless = filter (/= ' ')++testKeyringLookup :: FilePath -> String -> Bool -> Assertion+testKeyringLookup fpr eok expected = do+ kr <- loadKeyring fpr+ let foundKey = getOne (kr @= (read eok :: EightOctetKeyId))+ assertEqual (eok ++ " in " ++ fpr) expected (isJust foundKey)++testKeyringSize :: FilePath -> Int -> Assertion+testKeyringSize fpr expected = do+ kr <- loadKeyring fpr+ assertEqual ("key count in " ++ fpr) expected (size kr)++assertLeftContains+ :: String -> Either VerificationError a -> Assertion+assertLeftContains needle result =+ case result of+ Left err ->+ let rendered = renderVerificationError err+ in if needle `isInfixOf` rendered+ then pure ()+ else+ assertFailure+ ( "Expected error containing '"+ ++ needle+ ++ "', got '"+ ++ rendered+ ++ "'."+ )+ Right _ ->+ assertFailure+ ("Expected Left containing '" ++ needle ++ "', got Right.")++testMissingSigningKeyMessage :: FilePath -> FilePath -> Assertion+testMissingSigningKeyMessage keyring message = do+ kr <- loadKeyring keyring+ verification <-+ DC.runConduitRes $+ CB.sourceFile (fixturePath message)+ DC..| conduitGet get+ DC..| conduitDecompress+ DC..| conduitVerify kr Nothing+ DC..| CL.consume+ case verification of+ [] -> assertFailure "Expected at least one verification result."+ (firstResult : _) ->+ assertLeftContains "signing key not found in keyring" firstResult++testTamperedSignatureMessage :: FilePath -> FilePath -> Assertion+testTamperedSignatureMessage keyring message = do+ kr <- loadKeyring keyring+ packets <- loadAndDecompressPkts message+ let tamperedPackets = map tamperLiteral packets+ verification <-+ DC.runConduitRes $+ CL.sourceList tamperedPackets+ DC..| conduitVerify kr Nothing+ DC..| CL.consume+ case verification of+ [] -> assertFailure "Expected at least one verification result."+ (firstResult : _) ->+ assertLeftContains "signature mismatch" firstResult+ where+ tamperLiteral (LiteralDataPkt dt fn ts payload) =+ LiteralDataPkt dt fn ts (BL.snoc payload 0)+ tamperLiteral pkt = pkt++testRevokedCertificateMessage :: FilePath -> Assertion+testRevokedCertificateMessage keyfile = do+ ks <-+ DC.runConduitRes $+ CB.sourceFile ("tests/data/" ++ keyfile)+ DC..| conduitGet get+ DC..| conduitToTKsEither+ DC..| conduitDropErrorsAndNothings+ DC..| CL.consume+ assertLeftContains+ "signing key is revoked"+ ( mapM+ ( verifyUnknownTKWith+ (verifySigWith defaultVerificationPolicy (verifyAgainstKeys ks))+ Nothing+ )+ ks+ )++testKeysSelfVerification :: Bool -> FilePath -> Assertion+testKeysSelfVerification expectsuccess keyfile = do+ ks <-+ DC.runConduitRes $+ CB.sourceFile ("tests/data/" ++ keyfile)+ DC..| conduitGet get+ DC..| conduitToTKsEither+ DC..| conduitDropErrorsAndNothings+ DC..| CL.consume+ let verifieds =+ mapM+ ( verifyUnknownTKWith+ (verifySigWith defaultVerificationPolicy (verifyAgainstKeys ks))+ Nothing+ )+ ks+ assertEqual+ (keyfile ++ " self-verification")+ expectsuccess+ (isRight verifieds)++testKeysExpiration :: Bool -> FilePath -> Assertion+testKeysExpiration expectsuccess keyfile = do+ ks <-+ DC.runConduitRes $+ CB.sourceFile (fixturePath keyfile)+ DC..| conduitGet get+ DC..| conduitToTKsEither+ DC..| conduitDropErrorsAndNothings+ DC..| CL.consume+ case mapM+ ( verifyUnknownTKWith+ (verifySigWith defaultVerificationPolicy (verifyAgainstKeys ks))+ Nothing+ )+ ks of+ Left err ->+ assertFailure+ ( keyfile+ ++ " key self-verification failed before expiration check: "+ ++ renderVerificationError err+ )+ Right verifieds -> do+ let tvalid =+ all+ ( isTKTimeValid+ (posixSecondsToUTCTime (realToFrac (1400000000 :: Integer)))+ )+ verifieds+ assertEqual (keyfile ++ " key expiration") expectsuccess tvalid++testConduitVerifyProcessesSigV6 :: Assertion+testConduitVerifyProcessesSigV6 = do+ kr <- loadKeyring "pubring.gpg"+ let sigPayload =+ SigV6+ BinarySig+ RSA+ SHA256+ (SignatureSalt (BL.replicate 16 0))+ []+ []+ 0+ (MPI 0 :| [])+ packets =+ [ LiteralDataPkt+ BinaryData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ "payload"+ , SignaturePkt sigPayload+ ]+ verification <-+ DC.runConduitRes $+ CL.sourceList packets+ DC..| conduitVerify kr Nothing+ DC..| CL.consume+ case verification of+ [Left _] -> return ()+ other ->+ assertFailure+ ( "Expected one v6 verification result (failure is fine), got "+ ++ show (length other)+ ++ " results"+ )++testSubkeyBindingRejectsUnsupportedCriticalSubpacket :: Assertion+testSubkeyBindingRejectsUnsupportedCriticalSubpacket = do+ (primarySigner, primarySigningKey) <- loadUnencryptedRsaSigner+ (subkeySigner, _) <- loadDeterministicEd25519Signer+ let creationTime = addTimestampSeconds (_timestamp primarySigner) 10+ (hashed, unhashed) <-+ messageIssuerSubpacketsAt primarySigner creationTime+ let bindingPayload =+ payloadForSig+ SubkeyBindingSig+ emptyPSC+ { lastPrimaryKey = PublicKeyPkt primarySigner+ , lastSubkey = PublicSubkeyPkt subkeySigner+ }+ hashedWithUnsupportedCritical =+ SigSubPacket True (OtherSigSub 111 "unsupported-critical")+ : hashed+ keyring = [TKUnknown (primarySigner, Nothing) [] [] [] []]+ state =+ emptyPSC+ { lastPrimaryKey = PublicKeyPkt primarySigner+ , lastSubkey = PublicSubkeyPkt subkeySigner+ }+ sigPayload <-+ case signDataWithRSA+ SubkeyBindingSig+ primarySigningKey+ hashedWithUnsupportedCritical+ unhashed+ bindingPayload of+ Left err ->+ assertFailure+ ( "failed to sign subkey binding with unsupported critical subpacket: "+ ++ renderSignError err+ )+ >> fail "expected subkey binding signature"+ Right sig -> pure sig+ case verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeys keyring)+ (SignaturePkt sigPayload)+ state+ Nothing of+ Left (UnsupportedCriticalSubpacket SubkeyBindingSig) -> pure ()+ Left err ->+ assertFailure+ ( "Expected unsupported critical subpacket rejection for subkey binding signature, got: "+ ++ renderVerificationError err+ )+ Right _ ->+ assertFailure+ "Expected unsupported critical subpacket rejection for subkey binding signature, but verification succeeded"++testSubkeyBindingMissingBackSignatureProducesWarning :: Assertion+testSubkeyBindingMissingBackSignatureProducesWarning = do+ (primarySigner, primarySigningKey) <- loadUnencryptedRsaSigner+ (subkeySignerRaw, _) <- loadDeterministicEd25519Signer+ let subkeySigner = setKeyVersion V6 subkeySignerRaw+ creationTime = addTimestampSeconds (_timestamp primarySigner) 10+ keyring = [TKUnknown (primarySigner, Nothing) [] [] [] []]+ state =+ emptyPSC+ { lastPrimaryKey = PublicKeyPkt primarySigner+ , lastSubkey = PublicSubkeyPkt subkeySigner+ }+ sigPayload <-+ signSubkeyBindingWithRSAExtrasAt+ primarySigner+ subkeySigner+ primarySigningKey+ creationTime+ [SigSubPacket False (KeyFlags (Set.singleton SignDataKey))]+ case verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeys keyring)+ (SignaturePkt sigPayload)+ state+ Nothing of+ Left err ->+ assertFailure+ ( "Expected successful verification with warning for missing back-signature, got: "+ ++ renderVerificationError err+ )+ Right verification ->+ assertBool+ "v6 signing-capable subkey binding without embedded back-signature should emit warning"+ ( MissingSubkeyBackSignatureWarning+ `elem` _verificationWarnings verification+ )++testPrimaryKeyBindingRejectsUnsupportedCriticalSubpacket+ :: Assertion+testPrimaryKeyBindingRejectsUnsupportedCriticalSubpacket = do+ (signer, _) <- loadUnencryptedRsaSigner+ let sigPayload =+ SigV4+ PrimaryKeyBindingSig+ RSA+ SHA256+ [SigSubPacket True (OtherSigSub 111 "unsupported-critical")]+ []+ 0+ (MPI 0 :| [])+ verifyFn _ _ _ = Right (Verification signer sigPayload [])+ state =+ emptyPSC+ { lastPrimaryKey = PublicKeyPkt signer+ , lastSubkey = PublicSubkeyPkt signer+ }+ case verifySigWith+ defaultVerificationPolicy+ verifyFn+ (SignaturePkt sigPayload)+ state+ Nothing of+ Left (UnsupportedCriticalSubpacket PrimaryKeyBindingSig) -> pure ()+ Left err ->+ assertFailure+ ( "Expected unsupported critical subpacket rejection for primary key binding signature, got: "+ ++ renderVerificationError err+ )+ Right _ ->+ assertFailure+ "Expected unsupported critical subpacket rejection for primary key binding signature, but verification succeeded"++testSignatureBuilders :: Assertion+testSignatureBuilders = do+ (_, signingKey) <- loadUnencryptedRsaSigner+ -- Test RSA builder API with empty subpackets+ let builderRsa = sigBuilderInit @'PKA.RSA BinarySig SHA512+ let builderRsaWithHashed = addHashedSubs (listToHashedSubs []) builderRsa+ let builderRsaFinal = addUnhashedSubs (listToUnhashedSubs []) builderRsaWithHashed+ case signDataWithRSABuilder builderRsaFinal signingKey "test payload" of+ Left err ->+ assertFailure ("RSA builder API failed: " ++ renderSignError err)+ Right (SigV4 BinarySig RSA SHA512 _ _ _ _) -> pure ()+ Right other ->+ assertFailure+ ( "RSA builder API should generate BinarySig RSA SHA512, got "+ ++ show other+ )++ -- Test RSA builder API with non-default hash algorithm+ let builderRsaSha256 = sigBuilderInit @'PKA.RSA BinarySig SHA256+ builderRsaSha256WithHashed =+ addHashedSubs (listToHashedSubs []) builderRsaSha256+ builderRsaSha256Final =+ addUnhashedSubs+ (listToUnhashedSubs [])+ builderRsaSha256WithHashed+ case signDataWithRSABuilder+ builderRsaSha256Final+ signingKey+ "test payload" of+ Left err ->+ assertFailure+ ("RSA SHA256 builder API failed: " ++ renderSignError err)+ Right (SigV4 BinarySig RSA SHA256 _ _ _ _) ->+ pure ()+ Right other ->+ assertFailure+ ( "RSA builder API should generate BinarySig RSA SHA256, got "+ ++ show other+ )++ -- Unsupported hash algorithms should be rejected by RSA PKCS#1 v1.5 backend+ let builderRsaUnsupported = sigBuilderInit @'PKA.RSA BinarySig (OtherHA 99)+ builderRsaUnsupportedWithHashed =+ addHashedSubs (listToHashedSubs []) builderRsaUnsupported+ builderRsaUnsupportedFinal =+ addUnhashedSubs+ (listToUnhashedSubs [])+ builderRsaUnsupportedWithHashed+ case signDataWithRSABuilder+ builderRsaUnsupportedFinal+ signingKey+ "test payload" of+ Left (SignBackendError _) -> pure ()+ Left err ->+ assertFailure+ ( "RSA builder API should reject unsupported hash with backend error, got "+ ++ renderSignError err+ )+ Right sig ->+ assertFailure+ ( "RSA builder API should reject unsupported hash algorithm, got "+ ++ show sig+ )++ -- Runtime hash witness promotion should build RFC9580-compatible builders.+ let runtimeRsa =+ case sigBuilderInitRuntime @'PKA.RSA RFC9580 BinarySig SHA512 of+ Left err -> Left err+ Right builder ->+ let withHashed = addHashedSubs (listToHashedSubs []) builder+ withUnhashed = addUnhashedSubs (listToUnhashedSubs []) withHashed+ in first+ renderSignError+ (signDataWithRSABuilder withUnhashed signingKey "test payload")+ case runtimeRsa of+ Left err ->+ assertFailure+ ( "runtime RFC9580 SHA512 builder initialization should succeed, got "+ ++ err+ )+ Right (SigV4 BinarySig RSA SHA512 _ _ _ _) -> pure ()+ Right other ->+ assertFailure+ ( "runtime RFC9580 SHA512 builder should produce BinarySig RSA SHA512, got "+ ++ show other+ )++ case sigBuilderInitRuntime @'PKA.RSA RFC9580 BinarySig SHA1 of+ Left _ -> pure ()+ Right _ ->+ assertFailure+ "runtime RFC9580 builder initialization should reject deprecated SHA1"++ case sigBuilderInitRuntime @'PKA.RSA RFC9580 BinarySig (OtherHA 99) of+ Left _ -> pure ()+ Right _ ->+ assertFailure+ "runtime builder initialization should reject non-witness-backed hash algorithms"++ -- Test Ed25519 builder API with empty subpackets+ (_, edSigningKey) <- loadDeterministicEd25519Signer+ let builderEd25519 = sigBuilderInit @'PKA.Ed25519 BinarySig SHA512+ let builderEd25519WithHashed = addHashedSubs (listToHashedSubs []) builderEd25519+ let builderEd25519Final =+ addUnhashedSubs (listToUnhashedSubs []) builderEd25519WithHashed+ case signDataWithEd25519Builder+ builderEd25519Final+ edSigningKey+ "test payload" of+ Left err ->+ assertFailure+ ("Ed25519 builder API failed: " ++ renderSignError err)+ Right (SigV4 BinarySig PKA.Ed25519 SHA512 _ _ _ _) -> pure ()+ Right other ->+ assertFailure+ ( "Ed25519 builder API should generate BinarySig Ed25519 SHA512, got "+ ++ show other+ )++ -- Test Ed25519 v6 builder API+ let v6Salt = SignatureSalt (BL.replicate 32 0x42)+ let builderEd25519V6 = sigBuilderInitV6 @'PKA.Ed25519 BinarySig SHA512 v6Salt+ let builderEd25519V6WithHashed = addHashedSubs (listToHashedSubs []) builderEd25519V6+ let builderEd25519V6Final =+ addUnhashedSubs+ (listToUnhashedSubs [])+ builderEd25519V6WithHashed+ case signDataWithEd25519V6Builder+ builderEd25519V6Final+ edSigningKey+ "test payload" of+ Left err ->+ assertFailure+ ("Ed25519 v6 builder API failed: " ++ renderSignError err)+ Right (SigV6 BinarySig PKA.Ed25519 SHA512 salt _ _ _ _) ->+ assertEqual+ "Ed25519 v6 builder API should preserve 32-byte salt"+ 32+ (BL.length (unSignatureSalt salt))+ Right other ->+ assertFailure+ ( "Ed25519 v6 builder API should generate BinarySig Ed25519 SHA512 SigV6, got "+ ++ show other+ )++ -- Runtime v6 builder initialization should support SHA3 witness-backed hashes.+ let v6Sha3Salt = SignatureSalt (BL.replicate 16 0x33)+ runtimeEd25519V6Sha3 =+ case sigBuilderInitV6Runtime @'PKA.Ed25519+ RFC9580+ BinarySig+ SHA3_256+ v6Sha3Salt of+ Left err -> Left err+ Right builder ->+ let withHashed = addHashedSubs (listToHashedSubs []) builder+ withUnhashed = addUnhashedSubs (listToUnhashedSubs []) withHashed+ in first+ renderSignError+ ( signDataWithEd25519V6Builder+ withUnhashed+ edSigningKey+ "test payload"+ )+ case runtimeEd25519V6Sha3 of+ Left err ->+ assertFailure+ ("runtime RFC9580 SHA3-256 v6 builder should succeed, got " ++ err)+ Right (SigV6 BinarySig PKA.Ed25519 SHA3_256 salt _ _ _ _) ->+ assertEqual+ "runtime RFC9580 SHA3-256 v6 builder should preserve 16-byte salt"+ 16+ (BL.length (unSignatureSalt salt))+ Right other ->+ assertFailure+ ( "runtime RFC9580 SHA3-256 v6 builder should produce Ed25519 SigV6 SHA3_256, got "+ ++ show other+ )++testLegalSubpacketBuilders :: Assertion+testLegalSubpacketBuilders = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ issuerKeyId <-+ case eightOctetKeyID signer of+ Left err ->+ assertFailure+ ( "failed to derive issuer key id for legal-subpacket builder test: "+ ++ err+ )+ >> fail "expected issuer key id"+ Right i -> pure i++ let signerFp = fingerprint signer+ creation = ThirtyTwoBitTimeStamp 1+ v4Builder = sigBuilderInit @'PKA.RSA BinarySig SHA512+ v4WithHashed =+ addHashedSubs+ ( listToLegalSubs+ [ LegalSigCreationTime creation+ , LegalIssuerFingerprintV4 signerFp+ ]+ )+ v4Builder+ v4Final =+ addUnhashedSubs+ (listToLegalSubs [LegalIssuerV4 issuerKeyId])+ v4WithHashed++ case signDataWithRSABuilder+ v4Final+ signingKey+ "typed legal subpacket payload" of+ Left err ->+ assertFailure+ ("v4 legal-subpacket builder failed: " ++ renderSignError err)+ Right (SigV4 _ _ _ hashed unhashed _ _) -> do+ assertBool+ "v4 legal-subpacket builder should include creation time in hashed subpackets"+ (SigSubPacket False (SigCreationTime creation) `elem` hashed)+ assertBool+ "v4 legal-subpacket builder should include v4 issuer fingerprint in hashed subpackets"+ ( SigSubPacket+ False+ (IssuerFingerprint IssuerFingerprintV4 signerFp)+ `elem` hashed+ )+ assertBool+ "v4 legal-subpacket builder should include issuer key ID only in unhashed subpackets"+ (SigSubPacket False (Issuer issuerKeyId) `elem` unhashed)+ Right other ->+ assertFailure+ ( "v4 legal-subpacket builder should generate SigV4, got "+ ++ show other+ )++ (_, edSigningKey) <- loadDeterministicEd25519Signer+ let v6Salt = SignatureSalt (BL.replicate 32 0x17)+ v6Builder = sigBuilderInitV6 @'PKA.Ed25519 BinarySig SHA512 v6Salt+ v6WithHashed =+ addHashedSubs+ ( listToLegalSubs+ [ LegalSigCreationTime creation+ , LegalIssuerFingerprintV6 signerFp+ ]+ )+ v6Builder+ v6Final = addUnhashedSubs (listToLegalSubs []) v6WithHashed++ case signDataWithEd25519V6Builder+ v6Final+ edSigningKey+ "typed legal subpacket payload" of+ Left err ->+ assertFailure+ ("v6 legal-subpacket builder failed: " ++ renderSignError err)+ Right (SigV6 _ _ _ _ hashed unhashed _ _) -> do+ assertBool+ "v6 legal-subpacket builder should include creation time in hashed subpackets"+ (SigSubPacket False (SigCreationTime creation) `elem` hashed)+ assertBool+ "v6 legal-subpacket builder should include v6 issuer fingerprint in hashed subpackets"+ ( SigSubPacket+ False+ (IssuerFingerprint IssuerFingerprintV6 signerFp)+ `elem` hashed+ )+ assertEqual+ "v6 legal-subpacket builder should keep unhashed set empty for typed legal set used here"+ []+ unhashed+ Right other ->+ assertFailure+ ( "v6 legal-subpacket builder should generate SigV6, got "+ ++ show other+ )++testTemporaryKeyValidityRespectsLatestSelfSignature :: Assertion+testTemporaryKeyValidityRespectsLatestSelfSignature = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let uidText = "temporary-validity@example.org"+ uid = UserId uidText+ keyCreated = _timestamp signer+ longValidityCertTime = addTimestampSeconds keyCreated 10+ temporaryValidityCertTime = addTimestampSeconds keyCreated 20+ validCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 25)+ expiredCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 200)+ preExpirySignatureTime = addTimestampSeconds keyCreated 25+ postExpirySignatureTime = addTimestampSeconds keyCreated 40+ payload = "temporary validity signer timeline payload"+ longValidityCert <-+ signCertificationAt+ signer+ signingKey+ uid+ longValidityCertTime+ [SigSubPacket False (KeyExpirationTime 1000)]+ temporaryValidityCert <-+ signCertificationAt+ signer+ signingKey+ uid+ temporaryValidityCertTime+ [SigSubPacket False (KeyExpirationTime 30)]+ preExpirySignature <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ preExpirySignatureTime+ payload+ postExpirySignature <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ postExpirySignatureTime+ payload+ let tk =+ TKUnknown+ (signer, Nothing)+ []+ [(uidText, [longValidityCert, temporaryValidityCert])]+ []+ []+ keyring = mkTestKeyring [tk]+ assertBool+ "latest effective self-signature should keep key valid before temporary expiration"+ (isTKTimeValid validCheckTime tk)+ assertBool+ "latest effective self-signature should expire key at the temporary validity boundary"+ (not (isTKTimeValid expiredCheckTime tk))+ assertSingleSignerFingerprint+ "pre-expiry signatures should still verify"+ (fingerprint signer)+ (verifyTimelinePackets keyring payload preExpirySignature)+ assertSingleFailureContainsTimeline+ "post-expiry signatures should be rejected"+ "signing key was not valid at the signature creation time"+ (verifyTimelinePackets keyring payload postExpirySignature)++testTemporaryKeyValidityRejectsSelfSignatureGaps :: Assertion+testTemporaryKeyValidityRejectsSelfSignatureGaps = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let uidText = "temporary-validity-gap@example.org"+ uid = UserId uidText+ keyCreated = _timestamp signer+ firstCertTime = addTimestampSeconds keyCreated 10+ secondCertTime = addTimestampSeconds keyCreated 30+ gapCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 25)+ renewedCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 35)+ gapSignatureTime = addTimestampSeconds keyCreated 25+ renewedSignatureTime = addTimestampSeconds keyCreated 35+ payload = "temporary validity gap signer timeline payload"+ firstCertification <-+ signCertificationAt+ signer+ signingKey+ uid+ firstCertTime+ [ SigSubPacket False (KeyExpirationTime 1000)+ , SigSubPacket False (SigExpirationTime 10)+ ]+ secondCertification <-+ signCertificationAt+ signer+ signingKey+ uid+ secondCertTime+ [SigSubPacket False (KeyExpirationTime 1000)]+ gapSignature <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ gapSignatureTime+ payload+ renewedSignature <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ renewedSignatureTime+ payload+ let tk =+ TKUnknown+ (signer, Nothing)+ []+ [(uidText, [firstCertification, secondCertification])]+ []+ []+ keyring = mkTestKeyring [tk]+ assertBool+ "a gap with no effective self-signature should make the key temporarily invalid"+ (not (isTKTimeValid gapCheckTime tk))+ assertBool+ "a later self-signature should restore validity once it becomes effective"+ (isTKTimeValid renewedCheckTime tk)+ assertSingleFailureContainsTimeline+ "signatures made during a self-signature gap should be rejected"+ "signing key was not valid at the signature creation time"+ (verifyTimelinePackets keyring payload gapSignature)+ assertSingleSignerFingerprint+ "signatures made after a later self-signature becomes effective should verify"+ (fingerprint signer)+ (verifyTimelinePackets keyring payload renewedSignature)++testTemporaryKeyValidityRejectsPreCertificationGap :: Assertion+testTemporaryKeyValidityRejectsPreCertificationGap = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let uidText = "temporary-validity-pre-cert-gap@example.org"+ uid = UserId uidText+ keyCreated = _timestamp signer+ certificationTime = addTimestampSeconds keyCreated 20+ invalidCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 10)+ validCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 25)+ invalidSignatureTime = addTimestampSeconds keyCreated 10+ validSignatureTime = addTimestampSeconds keyCreated 25+ payload =+ "temporary validity pre-certification gap signer timeline payload"+ certification <-+ signCertificationAt+ signer+ signingKey+ uid+ certificationTime+ [SigSubPacket False (KeyExpirationTime 1000)]+ invalidSignature <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ invalidSignatureTime+ payload+ validSignature <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ validSignatureTime+ payload+ let tk =+ TKUnknown (signer, Nothing) [] [(uidText, [certification])] [] []+ keyring = mkTestKeyring [tk]+ assertBool+ "a key should be invalid before its first self-signature becomes effective"+ (not (isTKTimeValid invalidCheckTime tk))+ assertBool+ "a key should become valid once its first self-signature becomes effective"+ (isTKTimeValid validCheckTime tk)+ assertSingleFailureContainsTimeline+ "signatures made before the first self-signature should be rejected"+ "signing key was not valid at the signature creation time"+ (verifyTimelinePackets keyring payload invalidSignature)+ assertSingleSignerFingerprint+ "signatures made after the first self-signature becomes effective should verify"+ (fingerprint signer)+ (verifyTimelinePackets keyring payload validSignature)++testTemporaryKeyValidityFollowsTemporarySelfRevocation+ :: Assertion+testTemporaryKeyValidityFollowsTemporarySelfRevocation = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let uidText = "temporary-validity-self-revocation@example.org"+ uid = UserId uidText+ keyCreated = _timestamp signer+ certificationTime = addTimestampSeconds keyCreated 10+ revocationTime = addTimestampSeconds keyCreated 20+ revokedCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 25)+ restoredCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 35)+ revokedSignatureTime = addTimestampSeconds keyCreated 25+ restoredSignatureTime = addTimestampSeconds keyCreated 35+ payload = "temporary validity self-revocation signer timeline payload"+ certification <-+ signCertificationAt+ signer+ signingKey+ uid+ certificationTime+ [SigSubPacket False (KeyExpirationTime 1000)]+ temporaryRevocation <-+ signCertificationRevocationAt+ signer+ signingKey+ uid+ revocationTime+ [SigSubPacket False (SigExpirationTime 10)]+ revokedSignature <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ revokedSignatureTime+ payload+ restoredSignature <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ restoredSignatureTime+ payload+ let tk =+ TKUnknown+ (signer, Nothing)+ []+ [(uidText, [certification, temporaryRevocation])]+ []+ []+ keyring = mkTestKeyring [tk]+ assertBool+ "a temporary self-revocation should disable signing while it is effective"+ (not (isTKTimeValid revokedCheckTime tk))+ assertBool+ "a self-certification should become valid again once its temporary revocation expires"+ (isTKTimeValid restoredCheckTime tk)+ assertSingleFailureContainsTimeline+ "signatures made during a temporary self-revocation should be rejected"+ "signing key was not valid at the signature creation time"+ (verifyTimelinePackets keyring payload revokedSignature)+ assertSingleSignerFingerprint+ "signatures made after a temporary self-revocation expires should verify"+ (fingerprint signer)+ (verifyTimelinePackets keyring payload restoredSignature)++testTemporaryKeyValidityDoesNotReviveOlderSelfSignatures+ :: Assertion+testTemporaryKeyValidityDoesNotReviveOlderSelfSignatures = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let uidText = "temporary-validity-no-revival@example.org"+ uid = UserId uidText+ keyCreated = _timestamp signer+ firstCertTime = addTimestampSeconds keyCreated 10+ secondCertTime = addTimestampSeconds keyCreated 20+ invalidCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 30)+ invalidSignatureTime = addTimestampSeconds keyCreated 30+ payload = "temporary validity no revival signer timeline payload"+ firstCertification <-+ signCertificationAt+ signer+ signingKey+ uid+ firstCertTime+ [SigSubPacket False (KeyExpirationTime 1000)]+ secondCertification <-+ signCertificationAt+ signer+ signingKey+ uid+ secondCertTime+ [ SigSubPacket False (KeyExpirationTime 1000)+ , SigSubPacket False (SigExpirationTime 5)+ ]+ invalidSignature <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ invalidSignatureTime+ payload+ let tk =+ TKUnknown+ (signer, Nothing)+ []+ [(uidText, [firstCertification, secondCertification])]+ []+ []+ keyring = mkTestKeyring [tk]+ assertBool+ "an expired newer self-signature should not revive an older certification"+ (not (isTKTimeValid invalidCheckTime tk))+ assertSingleFailureContainsTimeline+ "signatures after a newer self-signature expires should be rejected until re-certified"+ "signing key was not valid at the signature creation time"+ (verifyTimelinePackets keyring payload invalidSignature)++testUidRetiredRevocationIsHistorical :: Assertion+testUidRetiredRevocationIsHistorical = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let uidText = "retired-uid@example.org"+ uid = UserId uidText+ keyCreated = _timestamp signer+ certificationTime = addTimestampSeconds keyCreated 10+ revocationTime = addTimestampSeconds keyCreated 20+ certification <-+ signCertificationAt signer signingKey uid certificationTime []+ retirementRevocation <-+ signCertificationRevocationAt+ signer+ signingKey+ uid+ revocationTime+ [ SigSubPacket+ False+ (ReasonForRevocation UserIdInfoNoLongerValid "")+ ]+ let tk =+ TKUnknown+ (signer, Nothing)+ []+ [(uidText, [certification, retirementRevocation])]+ []+ []+ verifyAt ts =+ verifyUnknownTKWith+ (verifySigWith defaultVerificationPolicy (verifyAgainstKeys [tk]))+ (Just (timestampToUTCTime ts))+ tk+ beforeRetirement <-+ case verifyAt (addTimestampSeconds keyCreated 15) of+ Left err ->+ assertFailure+ ( "uid-retired pre-revocation verification failed: "+ ++ renderVerificationError err+ )+ >> fail "expected verified transferable key before uid retirement"+ Right verifiedTK -> pure verifiedTK+ afterRetirement <-+ case verifyAt (addTimestampSeconds keyCreated 25) of+ Left err ->+ assertFailure+ ( "uid-retired post-revocation verification failed: "+ ++ renderVerificationError err+ )+ >> fail "expected verified transferable key after uid retirement"+ Right verifiedTK -> pure verifiedTK+ assertEqual+ "uid-retired revocation should preserve the UID before it becomes effective"+ 1+ (length (_tkuUIDs beforeRetirement))+ assertEqual+ "uid-retired revocation should retire the UID once effective"+ 0+ (length (_tkuUIDs afterRetirement))++testThirdPartyCertificationDoesNotAffectKeyValidityWindow+ :: Assertion+testThirdPartyCertificationDoesNotAffectKeyValidityWindow = do+ (targetSigner, _) <- loadUnencryptedRsaSigner+ (thirdPartySigner, thirdPartySigningKey) <-+ loadDeterministicEd25519Signer+ let uidText = "third-party-validity@example.org"+ uid = UserId uidText+ keyCreated = _timestamp targetSigner+ certificationTime = addTimestampSeconds keyCreated 10+ validityCheckTime = timestampToUTCTime (addTimestampSeconds keyCreated 200)+ thirdPartyCertification <-+ signCertificationWithEd25519At+ targetSigner+ thirdPartySigner+ thirdPartySigningKey+ uid+ certificationTime+ [SigSubPacket False (KeyExpirationTime 30)]+ let targetTK =+ TKUnknown+ (targetSigner, Nothing)+ []+ [(uidText, [thirdPartyCertification])]+ []+ []+ certifierTK = TKUnknown (thirdPartySigner, Nothing) [] [] [] []+ verifyAt =+ verifyUnknownTKWith+ ( verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeys [targetTK, certifierTK])+ )+ (Just validityCheckTime)+ targetTK+ verifiedTK <-+ case verifyAt of+ Left err ->+ assertFailure+ ( "third-party certification verification failed: "+ ++ renderVerificationError err+ )+ >> fail "expected verified transferable key"+ Right tk -> pure tk+ assertBool+ "third-party certifications must not define the primary key validity window"+ (isTKTimeValid validityCheckTime verifiedTK)++testCertificationRevocationOnlyRemovesMatchingSigner :: Assertion+testCertificationRevocationOnlyRemovesMatchingSigner = do+ (targetSigner, targetSigningKey) <- loadUnencryptedRsaSigner+ (thirdPartySigner, thirdPartySigningKey) <-+ loadDeterministicEd25519Signer+ let uidText = "cert-revocation-scope@example.org"+ uid = UserId uidText+ keyCreated = _timestamp targetSigner+ selfCertificationTime = addTimestampSeconds keyCreated 10+ thirdPartyCertificationTime = addTimestampSeconds keyCreated 12+ thirdPartyRevocationTime = addTimestampSeconds keyCreated 20+ beforeRevocationTime = timestampToUTCTime (addTimestampSeconds keyCreated 15)+ afterRevocationTime = timestampToUTCTime (addTimestampSeconds keyCreated 25)+ selfCertification <-+ signCertificationAt+ targetSigner+ targetSigningKey+ uid+ selfCertificationTime+ []+ thirdPartyCertification <-+ signCertificationWithEd25519At+ targetSigner+ thirdPartySigner+ thirdPartySigningKey+ uid+ thirdPartyCertificationTime+ []+ thirdPartyRevocation <-+ signCertificationRevocationWithEd25519At+ targetSigner+ thirdPartySigner+ thirdPartySigningKey+ uid+ thirdPartyRevocationTime+ []+ let targetTK =+ TKUnknown+ (targetSigner, Nothing)+ []+ [+ ( uidText+ ,+ [ selfCertification+ , thirdPartyCertification+ , thirdPartyRevocation+ ]+ )+ ]+ []+ []+ certifierTK = TKUnknown (thirdPartySigner, Nothing) [] [] [] []+ verifyAt ts =+ verifyUnknownTKWith+ ( verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeys [targetTK, certifierTK])+ )+ (Just ts)+ targetTK+ beforeRevocation <-+ case verifyAt beforeRevocationTime of+ Left err ->+ assertFailure+ ( "pre-revocation certification verification failed: "+ ++ renderVerificationError err+ )+ >> fail+ "expected verified transferable key before certification revocation"+ Right tk -> pure tk+ afterRevocation <-+ case verifyAt afterRevocationTime of+ Left err ->+ assertFailure+ ( "post-revocation certification verification failed: "+ ++ renderVerificationError err+ )+ >> fail+ "expected verified transferable key after certification revocation"+ Right tk -> pure tk+ let beforeSigs =+ maybe+ []+ snd+ (find ((== uidText) . fst) (_tkuUIDs beforeRevocation))+ afterSigs =+ maybe+ []+ snd+ (find ((== uidText) . fst) (_tkuUIDs afterRevocation))+ assertEqual+ "certification revocations should not apply before they become effective"+ [selfCertification, thirdPartyCertification]+ beforeSigs+ assertEqual+ "a certification revocation should remove only certifications from the same signer"+ [selfCertification]+ afterSigs++testPreferencesAtTimestamp :: Assertion+testPreferencesAtTimestamp = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let uid = UserId "prefs@example.org"+ UserId uidText = uid+ t1 = addTimestampSeconds (_timestamp signer) 1+ t2 = addTimestampSeconds t1 100+ t3 = addTimestampSeconds t2 100+ queryDuring = addTimestampSeconds t2 1+ queryAfterRevocation = addTimestampSeconds t3 1++ initialCertification <-+ signCertificationAt+ signer+ signingKey+ uid+ t1+ [SigSubPacket False (PreferredHashAlgorithms [SHA256])]+ updatedCertification <-+ signCertificationAt+ signer+ signingKey+ uid+ t2+ [SigSubPacket False (PreferredHashAlgorithms [SHA512])]+ certificationRevocation <-+ signCertificationRevocationAt signer signingKey uid t3 []++ let tk =+ TKUnknown+ (signer, Nothing)+ []+ [+ ( uidText+ ,+ [ initialCertification+ , updatedCertification+ , certificationRevocation+ ]+ )+ ]+ []+ []++ assertEqual+ "key preferences at timestamp should use the latest active self-certification"+ (Just [PreferredHashAlgorithms [SHA512]])+ (effectiveKeyPreferencesAtTimestamp queryDuring tk)+ assertEqual+ "UID preferences at timestamp should use the latest active self-certification"+ (Just [PreferredHashAlgorithms [SHA512]])+ (effectiveUIDPreferencesAtTimestamp queryDuring uidText tk)+ assertEqual+ "revoked UID certifications should not provide effective UID preferences"+ Nothing+ ( effectiveUIDPreferencesAtTimestamp+ queryAfterRevocation+ uidText+ tk+ )+ assertEqual+ "key preferences should become unavailable when no active self-certification remains"+ Nothing+ (effectiveKeyPreferencesAtTimestamp queryAfterRevocation tk)++testChangePrivateKeyPassphraseLegacy :: Assertion+testChangePrivateKeyPassphraseLegacy = do+ passphrase <- readPKIPassphrase+ packets <-+ DC.runConduitRes $+ CB.sourceFile "tests/data/aes256-sha512.seckey"+ DC..| conduitGet get+ DC..| CL.consume+ (pkp, ska) <-+ case packets of+ (SecretKeyPkt pkpayload skaddendum : _) -> pure (pkpayload, skaddendum)+ _ ->+ assertFailure+ "aes256-sha512.seckey did not begin with a secret key packet"+ >> fail "expected secret key packet"+ originalDecrypted <-+ case decryptPrivateKey (pkp, ska) passphrase of+ Left err ->+ assertFailure ("decrypting original legacy key failed: " ++ err)+ >> fail "decrypting original legacy key failed"+ Right x -> pure x+ originalSKey <-+ case originalDecrypted of+ SUUnencrypted skey _ -> pure skey+ _ ->+ assertFailure+ "original legacy key should decrypt to unencrypted secret material"+ >> fail "expected unencrypted secret key"+ changed <-+ case changePrivateKeyPassphrase+ (pkp, ska)+ passphrase+ (Salt "12345678")+ (IV "1234567890ABCDEF")+ "changed-pki-password" of+ Left err ->+ assertFailure+ ( "legacy passphrase change should preserve the existing protection envelope, got: "+ ++ err+ )+ >> fail "legacy passphrase change failed"+ Right skaddendum -> pure skaddendum+ originalIterCount <-+ case ska of+ SUSSHA1 AES256 (IteratedSalted SHA512 _ iter) _ _ -> pure iter+ _ ->+ assertFailure+ "legacy key fixture should use SUSSHA1/AES256/IteratedSalted SHA512"+ >> fail "expected legacy key fixture"+ case changed of+ SUSSHA1 AES256 (IteratedSalted SHA512 _ iter) _ encryptedPayload -> do+ assertEqual+ "legacy passphrase change should preserve the S2K iteration count"+ originalIterCount+ iter+ assertBool+ "legacy passphrase change should emit encrypted payload"+ (not (BL.null encryptedPayload))+ _ ->+ assertFailure+ "legacy passphrase change should preserve the SUSSHA1/AES256 envelope"+ decrypted <-+ case decryptPrivateKey (pkp, changed) "changed-pki-password" of+ Left err ->+ assertFailure+ ("re-decrypting changed legacy key failed: " ++ err)+ >> fail "re-decrypting changed legacy key failed"+ Right x -> pure x+ case decrypted of+ SUUnencrypted skey _ ->+ assertEqual+ "legacy passphrase change should preserve secret key material"+ originalSKey+ skey+ _ ->+ assertFailure+ "legacy passphrase change should decrypt back to unencrypted secret material"++testChangePrivateKeyPassphraseV6 :: Assertion+testChangePrivateKeyPassphraseV6 = do+ oldPassphrase <- readPKIPassphrase+ armors <- loadArmor "v6-encrypted-secret.pgp.aa"+ armor <-+ case armors of+ (a : _) -> pure a+ [] ->+ assertFailure+ "v6-encrypted-secret.pgp.aa should contain one armored payload"+ >> fail "expected one armored payload"+ let packets = parsePkts (armorPayload armor)+ (pkp, ska) <-+ case packets of+ (SecretKeyPkt pkpayload skaddendum : _) -> pure (pkpayload, skaddendum)+ _ ->+ assertFailure+ "v6-encrypted-secret.pgp.aa did not begin with a secret key packet"+ >> fail "expected secret key packet"+ let newPassphrase = "changed-pki-password"+ originalDecrypted <-+ case decryptPrivateKey (pkp, ska) oldPassphrase of+ Left err ->+ assertFailure ("decrypting original v6 key failed: " ++ err)+ >> fail "decrypting original v6 key failed"+ Right x -> pure x+ originalSKey <-+ case originalDecrypted of+ SUUnencrypted skey _ -> pure skey+ _ ->+ assertFailure+ "original v6 key should decrypt to unencrypted secret material"+ >> fail "expected unencrypted secret key"+ let changedResult =+ changePrivateKeyPassphrase+ (pkp, ska)+ oldPassphrase+ (Salt "1234567890ABCDEF")+ (IV "1234567890ABCDE")+ newPassphrase+ changed <-+ case changedResult of+ Left err ->+ assertFailure ("changing v6 key passphrase failed: " ++ err)+ >> pure ska+ Right skaddendum -> pure skaddendum+ case changed of+ SUSAEAD AES256 OCB (Argon2 _ t p em) _ encryptedPayload -> do+ assertEqual+ "changed v6 key addendum should use expected Argon2 t parameter"+ 1+ t+ assertEqual+ "changed v6 key addendum should use expected Argon2 p parameter"+ 4+ p+ assertEqual+ "changed v6 key addendum should use expected Argon2 encoded memory parameter"+ 15+ em+ assertBool+ "changed v6 key addendum should contain encrypted key material"+ (not (BL.null encryptedPayload))+ _ ->+ assertFailure+ "changed v6 key addendum should be encrypted with SUSAEAD"+ let decryptedResult = decryptPrivateKey (pkp, changed) newPassphrase+ decrypted <-+ case decryptedResult of+ Left err ->+ assertFailure+ ("decryption with changed v6 passphrase failed: " ++ err)+ >> fail "decryption with changed v6 passphrase failed"+ Right x -> pure x+ case decrypted of+ SUUnencrypted skey _ ->+ assertEqual+ "changing a v6 key passphrase should preserve secret key material"+ originalSKey+ skey+ _ ->+ assertFailure+ "changed v6 key should decrypt with the new passphrase"++testPolicySignatureContextValidation :: Assertion+testPolicySignatureContextValidation = do+ let pkSigV4PK = SigV4 KeyRevocationSig RSA SHA512 [] [] 0 (MPI 0 :| [])+ pkSigV4Direct = SigV4 SignatureDirectlyOnAKey RSA SHA512 [] [] 0 (MPI 0 :| [])+ pkSigV4Binding = SigV4 SubkeyBindingSig RSA SHA512 [] [] 0 (MPI 0 :| [])+ pkSigV6PK =+ SigV6+ KeyRevocationSig+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x11))+ []+ []+ 0+ (MPI 0 :| [])+ pkSigV6Direct =+ SigV6+ SignatureDirectlyOnAKey+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x12))+ []+ []+ 0+ (MPI 0 :| [])+ pkSigV6Binding =+ SigV6+ SubkeyBindingSig+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x13))+ []+ []+ 0+ (MPI 0 :| [])+ skSigV4Binding = SigV4 SubkeyBindingSig RSA SHA512 [] [] 0 (MPI 0 :| [])+ skSigV4Revocation = SigV4 SubkeyRevocationSig RSA SHA512 [] [] 0 (MPI 0 :| [])+ skSigV4Direct = SigV4 SignatureDirectlyOnAKey RSA SHA512 [] [] 0 (MPI 0 :| [])+ skSigV6Binding =+ SigV6+ SubkeyBindingSig+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x14))+ []+ []+ 0+ (MPI 0 :| [])+ skSigV6Revocation =+ SigV6+ SubkeyRevocationSig+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x15))+ []+ []+ 0+ (MPI 0 :| [])+ skSigV6Direct =+ SigV6+ SignatureDirectlyOnAKey+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x16))+ []+ []+ 0+ (MPI 0 :| [])+ uidSigV4Generic = SigV4 GenericCert RSA SHA512 [] [] 0 (MPI 0 :| [])+ uidSigV4Persona = SigV4 PersonaCert RSA SHA512 [] [] 0 (MPI 0 :| [])+ uidSigV4Casual = SigV4 CasualCert RSA SHA512 [] [] 0 (MPI 0 :| [])+ uidSigV4Positive = SigV4 PositiveCert RSA SHA512 [] [] 0 (MPI 0 :| [])+ uidSigV4CertRev = SigV4 CertRevocationSig RSA SHA512 [] [] 0 (MPI 0 :| [])+ uidSigV4Binding = SigV4 SubkeyBindingSig RSA SHA512 [] [] 0 (MPI 0 :| [])+ uidSigV6Generic =+ SigV6+ GenericCert+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x17))+ []+ []+ 0+ (MPI 0 :| [])+ uidSigV6Persona =+ SigV6+ PersonaCert+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x18))+ []+ []+ 0+ (MPI 0 :| [])+ uidSigV6Casual =+ SigV6+ CasualCert+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x19))+ []+ []+ 0+ (MPI 0 :| [])+ uidSigV6Positive =+ SigV6+ PositiveCert+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x1a))+ []+ []+ 0+ (MPI 0 :| [])+ uidSigV6CertRev =+ SigV6+ CertRevocationSig+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x1b))+ []+ []+ 0+ (MPI 0 :| [])+ uidSigV6Binding =+ SigV6+ SubkeyBindingSig+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x1c))+ []+ []+ 0+ (MPI 0 :| [])+ assertTrue+ "KeyRevocationSig allowed on primary key"+ (isAllowedPrimaryKeySig pkSigV4PK)+ assertTrue+ "SignatureDirectlyOnAKey allowed on primary key"+ (isAllowedPrimaryKeySig pkSigV4Direct)+ assertFalse+ "SubkeyBindingSig not allowed on primary key"+ (isAllowedPrimaryKeySig pkSigV4Binding)+ assertTrue+ "v6 KeyRevocationSig allowed on primary key"+ (isAllowedPrimaryKeySig pkSigV6PK)+ assertTrue+ "v6 SignatureDirectlyOnAKey allowed on primary key"+ (isAllowedPrimaryKeySig pkSigV6Direct)+ assertFalse+ "v6 SubkeyBindingSig not allowed on primary key"+ (isAllowedPrimaryKeySig pkSigV6Binding)+ assertTrue+ "SubkeyBindingSig allowed on subkey"+ (isAllowedSubkeySig skSigV4Binding)+ assertTrue+ "SubkeyRevocationSig allowed on subkey"+ (isAllowedSubkeySig skSigV4Revocation)+ assertFalse+ "SignatureDirectlyOnAKey not allowed on subkey"+ (isAllowedSubkeySig skSigV4Direct)+ assertTrue+ "v6 SubkeyBindingSig allowed on subkey"+ (isAllowedSubkeySig skSigV6Binding)+ assertTrue+ "v6 SubkeyRevocationSig allowed on subkey"+ (isAllowedSubkeySig skSigV6Revocation)+ assertFalse+ "v6 SignatureDirectlyOnAKey not allowed on subkey"+ (isAllowedSubkeySig skSigV6Direct)+ assertTrue+ "GenericCert allowed on UID"+ (isAllowedUIDSig uidSigV4Generic)+ assertTrue+ "PersonaCert allowed on UID"+ (isAllowedUIDSig uidSigV4Persona)+ assertTrue+ "CasualCert allowed on UID"+ (isAllowedUIDSig uidSigV4Casual)+ assertTrue+ "PositiveCert allowed on UID"+ (isAllowedUIDSig uidSigV4Positive)+ assertTrue+ "CertRevocationSig allowed on UID"+ (isAllowedUIDSig uidSigV4CertRev)+ assertFalse+ "SubkeyBindingSig not allowed on UID"+ (isAllowedUIDSig uidSigV4Binding)+ assertTrue+ "v6 GenericCert allowed on UID"+ (isAllowedUIDSig uidSigV6Generic)+ assertTrue+ "v6 PersonaCert allowed on UID"+ (isAllowedUIDSig uidSigV6Persona)+ assertTrue+ "v6 CasualCert allowed on UID"+ (isAllowedUIDSig uidSigV6Casual)+ assertTrue+ "v6 PositiveCert allowed on UID"+ (isAllowedUIDSig uidSigV6Positive)+ assertTrue+ "v6 CertRevocationSig allowed on UID"+ (isAllowedUIDSig uidSigV6CertRev)+ assertFalse+ "v6 SubkeyBindingSig not allowed on UID"+ (isAllowedUIDSig uidSigV6Binding)++testVerifySigWithV6RevocationRejectsLegacyIssuerKeyID+ :: Assertion+testVerifySigWithV6RevocationRejectsLegacyIssuerKeyID = do+ (signer, _) <- loadUnencryptedRsaSigner+ issuerKeyId <-+ case eightOctetKeyID signer of+ Left err ->+ assertFailure ("failed to derive RSA issuer key id: " ++ err)+ >> fail "expected issuer key id"+ Right i -> pure i+ let sigPayload =+ SigV6+ KeyRevocationSig+ RSA+ SHA256+ (SignatureSalt (BL.replicate 16 0))+ [SigSubPacket False (Issuer issuerKeyId)]+ []+ 0+ (MPI 0 :| [])+ verifyFn _ _ _ = Right (Verification signer sigPayload [])+ result =+ verifySigWith+ defaultVerificationPolicy+ verifyFn+ (SignaturePkt sigPayload)+ emptyPSC {lastPrimaryKey = PublicKeyPkt signer}+ Nothing+ case result of+ Left err ->+ assertBool+ "v6 revocation verification should reject legacy Issuer key-id subpackets"+ ( "Issuer Key ID subpacket is prohibited in v6 signatures"+ `isInfixOf` renderVerificationError err+ )+ Right _ ->+ assertFailure+ "Expected v6 revocation verification to reject legacy Issuer key-id subpacket"++testVerifySigWithV6RevocationRejectsLegacyIssuerKeyIDInUnhashed+ :: Assertion+testVerifySigWithV6RevocationRejectsLegacyIssuerKeyIDInUnhashed = do+ (signer, _) <- loadUnencryptedRsaSigner+ issuerKeyId <-+ case eightOctetKeyID signer of+ Left err ->+ assertFailure ("failed to derive RSA issuer key id: " ++ err)+ >> fail "expected issuer key id"+ Right i -> pure i+ let sigPayload =+ SigV6+ KeyRevocationSig+ RSA+ SHA256+ (SignatureSalt (BL.replicate 16 0))+ []+ [SigSubPacket False (Issuer issuerKeyId)]+ 0+ (MPI 0 :| [])+ verifyFn _ _ _ = Right (Verification signer sigPayload [])+ result =+ verifySigWith+ defaultVerificationPolicy+ verifyFn+ (SignaturePkt sigPayload)+ emptyPSC {lastPrimaryKey = PublicKeyPkt signer}+ Nothing+ case result of+ Left err ->+ assertBool+ "v6 revocation verification should reject legacy Issuer key-id subpackets in unhashed area"+ ( "Issuer Key ID subpacket is prohibited in v6 signatures"+ `isInfixOf` renderVerificationError err+ )+ Right _ ->+ assertFailure+ "Expected v6 revocation verification to reject legacy Issuer key-id subpacket in unhashed area"++testVerifySigWithV6RevocationAcceptsMatchingIssuerFingerprint+ :: Assertion+testVerifySigWithV6RevocationAcceptsMatchingIssuerFingerprint = do+ (signer, _) <- loadUnencryptedRsaSigner+ let sigPayload =+ SigV6+ KeyRevocationSig+ RSA+ SHA256+ (SignatureSalt (BL.replicate 16 0))+ [ SigSubPacket+ False+ (IssuerFingerprint IssuerFingerprintV6 (fingerprint signer))+ ]+ []+ 0+ (MPI 0 :| [])+ verifyFn _ _ _ = Right (Verification signer sigPayload [])+ result =+ verifySigWith+ defaultVerificationPolicy+ verifyFn+ (SignaturePkt sigPayload)+ emptyPSC {lastPrimaryKey = PublicKeyPkt signer}+ Nothing+ case result of+ Left err ->+ assertFailure+ ( "Expected matching IssuerFingerprint to verify for v6 revocation signature, got: "+ ++ renderVerificationError err+ )+ Right _ -> pure ()++testV6SignerConstructorRejectsV4Key :: Assertion+testV6SignerConstructorRejectsV4Key = do+ (signer, signingKey) <- loadDeterministicEd25519Signer+ case asV6PKPayload signer of+ Left _ -> pure ()+ Right signerV6 ->+ seq (mkEd25519SignerV6 signerV6 signingKey) $+ assertFailure+ "asV6PKPayload should reject v4 Ed25519 key payloads"+ (rsaSigner, rsaSigningKey) <- loadUnencryptedRsaSigner+ case asV6PKPayload rsaSigner of+ Left _ -> pure ()+ Right signerV6 ->+ seq (mkRSASignerV6 signerV6 rsaSigningKey) $+ assertFailure "asV6PKPayload should reject v4 RSA key payloads"++testGetSecretKeyHandlesV4X25519ECDHPubkey :: Assertion+testGetSecretKeyHandlesV4X25519ECDHPubkey = do+ let pkp =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ X25519+ ( ECDHPubKey+ ( EdDSAPubKey+ EdSigningCurve25519+ ( PrefixedNativeEPoint+ (EPoint (os2ip (B.cons 0x40 (B.replicate 32 0x01))))+ )+ )+ SHA256+ AES128+ )+ encodedSecret = runPut (put (MPI 42))+ case runGetOrFail (getSecretKey pkp) encodedSecret of+ Left (_, _, err) ->+ assertFailure+ ( "getSecretKey should parse v4 X25519 secret key material: "+ ++ err+ )+ Right (_, _, ECDHPrivateKey (ECDSA_PrivateKey prv)) ->+ assertEqual+ "getSecretKey should preserve X25519 private scalar"+ 42+ (ECDSA.private_d prv)+ Right (_, _, other) ->+ assertFailure+ ("unexpected parsed secret key type: " ++ show other)++testV4Ed25519SecretKeyRoundTripPreservesLeadingZeroByte+ :: Assertion+testV4Ed25519SecretKeyRoundTripPreservesLeadingZeroByte = do+ let secretBytes = B.pack (0 : replicate 31 1)+ pkt =+ SecretKeyPkt+ ( PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ EdDSA+ ( EdDSAPubKey+ EdSigningCurve25519+ ( PrefixedNativeEPoint+ (EPoint (os2ip (B.cons 0x40 (B.replicate 32 0x01))))+ )+ )+ )+ (SUUnencrypted (EdDSAPrivateKey EdSigningCurve25519 secretBytes) 0)+ case runGetOrFail (get :: Get Pkt) (runPut (put pkt)) of+ Left (_, _, err) ->+ assertFailure+ ("v4 Ed25519 secret key should round-trip: " ++ err)+ Right (_, _, parsedPkt) ->+ case parsedPkt of+ SecretKeyPkt+ _+ (SUUnencrypted (EdDSAPrivateKey EdSigningCurve25519 parsedBytes) _) ->+ assertEqual+ "v4 Ed25519 secret-key round-trip should preserve MPI-encoded bytes"+ secretBytes+ parsedBytes+ _ ->+ assertFailure+ ("unexpected parsed packet shape: " ++ show parsedPkt)++testV4Ed448SecretKeyRoundTripPreservesLeadingZeroByte+ :: Assertion+testV4Ed448SecretKeyRoundTripPreservesLeadingZeroByte = do+ let secretBytes = B.pack (0 : replicate 56 2)+ pkp =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ EdDSA+ ( EdDSAPubKey+ EdSigningCurve448+ ( PrefixedNativeEPoint+ (EPoint (os2ip (B.cons 0x40 (B.replicate 57 0x02))))+ )+ )+ encodedSecret = runPut (put (MPI (os2ip secretBytes)))+ case runGetOrFail (getSecretKey pkp) encodedSecret of+ Left (_, _, err) ->+ assertFailure+ ( "v4 Ed448 secret key should parse with preserved padding: "+ ++ err+ )+ Right (_, _, parsedSecret) ->+ assertEqual+ "v4 Ed448 secret-key parsing should restore the curve-width byte string"+ (EdDSAPrivateKey EdSigningCurve448 secretBytes)+ parsedSecret++testMkUnencryptedSKAddendumComputesLegacyChecksum :: Assertion+testMkUnencryptedSKAddendumComputesLegacyChecksum = do+ packets <-+ DC.runConduitRes $+ CB.sourceFile "tests/data/unencrypted.seckey"+ DC..| conduitGet get+ DC..| CL.consume+ case packets of+ (SecretKeyPkt pkp (SUUnencrypted sk _) : _) ->+ case mkUnencryptedSKAddendum pkp sk of+ Left err ->+ assertFailure+ ("mkUnencryptedSKAddendum should succeed for fixture key: " ++ err)+ Right (SUUnencrypted _ actualChecksum) -> do+ skPayload <-+ case putSKeyForPKPayload pkp sk of+ Left err ->+ assertFailure+ ( "failed to serialize secret key payload for checksum expectation: "+ ++ err+ )+ >> fail "expected serializable secret key payload"+ Right payload -> pure payload+ let expectedChecksum =+ fromIntegral+ ( BL.foldl'+ (\acc octet -> (acc + fromIntegral octet) `mod` (65536 :: Integer))+ 0+ (runPut skPayload)+ :: Integer+ )+ assertEqual+ "mkUnencryptedSKAddendum should use OpenPGP 16-bit checksum over serialized key material"+ expectedChecksum+ actualChecksum+ Right other ->+ assertFailure ("unexpected addendum constructor: " ++ show other)+ _ ->+ assertFailure+ "unencrypted.seckey did not begin with an unencrypted secret key packet"++testMkUnencryptedSKAddendumUsesV6ChecksumConvention :: Assertion+testMkUnencryptedSKAddendumUsesV6ChecksumConvention = do+ (pkp, rsaPrivateKey) <- loadUnencryptedRsaSignerV6+ case mkUnencryptedSKAddendum+ pkp+ (RSAPrivateKey (RSA_PrivateKey rsaPrivateKey)) of+ Left err ->+ assertFailure+ ("mkUnencryptedSKAddendum should support v6 key payloads: " ++ err)+ Right (SUUnencrypted _ checksum) ->+ assertEqual+ "v6 unencrypted addendum checksum should be zero"+ 0+ checksum+ Right other ->+ assertFailure ("unexpected addendum constructor: " ++ show other)++testReinterpretUnknownSKeyForPKPayloadDecodesEdDSA :: Assertion+testReinterpretUnknownSKeyForPKPayloadDecodesEdDSA = do+ let secretBytes = B.pack (0 : replicate 31 1)+ pkp =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ EdDSA+ ( EdDSAPubKey+ EdSigningCurve25519+ ( PrefixedNativeEPoint+ (EPoint (os2ip (B.cons 0x40 (B.replicate 32 0x01))))+ )+ )+ unknown = UnknownSKey (runPut (put (MPI (os2ip secretBytes))))+ case reinterpretUnknownSKeyForPKPayload pkp unknown of+ Left err ->+ assertFailure+ ( "reinterpretUnknownSKeyForPKPayload should decode Ed25519 key: "+ ++ err+ )+ Right skey ->+ assertEqual+ "reinterpretUnknownSKeyForPKPayload should return typed EdDSA secret key"+ (EdDSAPrivateKey EdSigningCurve25519 secretBytes)+ skey++testFromPrimaryKeyPktToTKUnknownRejectsSubkey :: Assertion+testFromPrimaryKeyPktToTKUnknownRejectsSubkey = do+ packets <-+ DC.runConduitRes $+ CB.sourceFile "tests/data/unencrypted.seckey"+ DC..| conduitGet get+ DC..| CL.consume+ case packets of+ (SecretKeyPkt pkp _ : _) ->+ case fromPrimaryKeyPktToTKUnknown (PublicSubkeyPkt pkp) of+ Left err ->+ assertBool+ "fromPrimaryKeyPktToTKUnknown should report non-primary packet tags"+ ("expected primary key packet" `isInfixOf` err)+ Right _ ->+ assertFailure+ "fromPrimaryKeyPktToTKUnknown should reject subkey packets"+ _ ->+ assertFailure+ "unencrypted.seckey did not begin with a secret key packet"
tests/Tests/MessageAndArmor.hs view
@@ -2,1940 +2,2640 @@ -- 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 TypeApplications #-}--module Tests.MessageAndArmor (messageAndArmorTests) where--import Codec.Encryption.OpenPGP.ASCIIArmor.Types (Armor(..), ArmorType(..))-import Codec.Encryption.OpenPGP.BlockCipher (keySize)-import Codec.Encryption.OpenPGP.CFB (decryptPreservingNonce, validateSEIPD1MDC)-import Codec.Encryption.OpenPGP.Encrypt (encryptSEIPDv2WithSKESKBlock)-import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)-import Codec.Encryption.OpenPGP.Internal (emptyPSC, lastLD)-import Codec.Encryption.OpenPGP.KeyringParser (parseUnknownTKs)-import Codec.Encryption.OpenPGP.Message-import Codec.Encryption.OpenPGP.Policy (signatureV6SaltSizeForHashAlgorithm)-import Codec.Encryption.OpenPGP.S2K (renderS2KError, string2Key)-import Codec.Encryption.OpenPGP.Serialize- ( armorPayloadsOfType- , parsePkts- , parsePktsEither- , recommendedArmorType- , singleClearSignedBlock- , singleArmorPayloadOfType- )-import Codec.Encryption.OpenPGP.SerializeForSigs (payloadForSig, payloadForSigWith)-import Codec.Encryption.OpenPGP.Signatures- ( renderSignError- , renderVerificationError- , signCertRevocationWithRSA- , signCertificationWithRSA- , signDataWithEd25519- , signDataWithEd25519V6- , signDataWithEd448- , signDataWithEd448V6- , signDataWithRSA- , signDataWithRSABuilder- , signDataWithEd25519Builder- , signDataWithEd25519V6Builder- , signDirectKeyWithRSA- , signKeyRevocationWithRSA- , signSubkeyRevocationWithRSA- , SignError(..)- , verifyAgainstKeyring- , verifyAgainstKeys- , verifySigWith- , VerificationError(..)- )-import Codec.Encryption.OpenPGP.Subpackets- ( addHashedSubs- , addUnhashedSubs- , listToHashedSubs- , listToUnhashedSubs- , sbTextNormMode- , sigBuilderInit- , TextNormalizationMode(..)- )-import Codec.Encryption.OpenPGP.Types-import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as PKA-import qualified Crypto.PubKey.RSA as RSA-import qualified Crypto.PubKey.Ed25519 as Ed25519-import Data.Binary (get, put)-import Data.Binary.Get (Get, runGetOrFail)-import Data.Binary.Put (putByteString, putWord16be, putWord32be, putWord8, runPut)-import Data.Bits (xor)-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import Data.Conduit.OpenPGP.Message (verifyMessage, verifyMessagePackets)-import Data.Conduit.OpenPGP.Verify (VerificationMode(..))-import Data.Either (isLeft, isRight)-import Data.List (isInfixOf)-import qualified Data.List.NonEmpty as NE-import Test.Tasty (TestTree, testGroup)-import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, testCase)-import Tests.Common- ( addTimestampSeconds- , assertSingleFailureContainsTimeline- , assertSingleSignerFingerprint- , encryptMessageDefault- , expectV4PKPayload- , expectV6PKPayload- , extractV4SignatureAlgorithmFields- , fp- , loadAndDecompressPkts- , loadArmor- , loadKeyring- , loadDeterministicEd25519Signer- , loadDeterministicEd25519SignerV6- , loadDeterministicEd448Signer- , loadDeterministicEd448SignerV6- , loadUnencryptedRsaSigner- , loadUnencryptedRsaSignerV6- , messageIssuerSubpacketsAt- , mkTestKeyring- , readFixtureLazy- , setPKAlgorithm- , signBinaryMessageWithRSAAt- , signBinaryMessageWithEd25519At- , signKeyRevocationWithReasonAt- , signKeyRevocationWithReasonAndExtrasAt- , signSubkeyBindingWithRSAAt- , signSubkeyRevocationWithRSAAt- , verifyTimelinePackets- )--messageAndArmorTests :: TestTree-messageAndArmorTests =- testGroup- "Message API and armor fixtures"- [ testGroup- "Message API group"- [ testCase "default encrypt/decrypt message" testDefaultedEncryptDecrypt- , testCase "specific encrypt/decrypt message" testExplicitEncryptDecrypt- , testCase- "specific encrypt can expose effective session material"- testExplicitEncryptExposesEffectiveSessionMaterial- , testCase- "default encrypt does not expose session material"- testDefaultEncryptDoesNotExposeSessionMaterial- , testCase "AES-128 EAX encrypt/decrypt message" testExplicitAES128EAXEncryptDecrypt- , testCase "AES-128 GCM encrypt/decrypt message" testExplicitAES128GCMEncryptDecrypt- , testCase- "specific encrypt rejects deprecated S2K hash in modern mode"- testExplicitEncryptRejectsDeprecatedS2KHash- , testCase "legacy fallback encrypt/decrypt message" testLegacyFallbackEncryptDecrypt- , testCase- "RFC4880 encryptMessage SEIPDv1 cleartext parses without trailing junk"- testRFC4880EncryptMessageSEIPDv1ParsesCleanly- , testCase "decryptMessage returns typed parse failures" testDecryptMessageTypedParseFailure- , testCase- "decryptMessage rejects unknown critical packets"- testDecryptMessageRejectsUnknownCriticalPacket- , testCase "decryptMessage returns typed decrypt failures" testDecryptMessageTypedDecryptFailure- , testCase- "decryptMessage rejects SEIPDv1 MDC tampering"- testDecryptMessageSEIPDv1MDCTampering- , testCase "sign message shape" testSignMessageShape- , testCase "sign message shape (RSA SigV6)" testSignMessageRSAV6- , testCase "sign message shape (Ed25519)" testSignMessageEd25519- , testCase "sign message shape (Ed25519 SigV6)" testSignMessageEd25519V6- , testCase "sign message shape (Ed448)" testSignMessageEd448- , testCase "sign message shape (Ed448 SigV6)" testSignMessageEd448V6- , testCase- "v4 Ed25519/Ed448 signatures use native fixed-width encoding"- testV4EdSignaturesUseNativeFixedWidthEncoding- , testCase- "v4 Ed25519Legacy key parsing rejects missing 0x40 prefix"- testV4Ed25519LegacyKeyRejectsMissingPrefix- , testCase- "detached v4 Ed25519 verification tolerates missing issuer hints"- testVerifyDetachedEd25519WithoutIssuerHints- , testCase- "detached v4 Ed25519 verification tolerates fake issuer hints"- testVerifyDetachedEd25519WithFakeIssuerHint- , testCase- "detached v4 Ed25519 verifyAgainstKeys tolerates missing issuer hints"- testVerifyDetachedEd25519WithoutIssuerHintsAgainstKeys- , testCase- "detached v4 Ed25519 verifyAgainstKeys tolerates fake issuer hints"- testVerifyDetachedEd25519WithFakeIssuerHintAgainstKeys- , testCase- "v4 EdDSA signatures verify with Ed25519 key algorithm identifier"- testVerifyV4EdDSASignatureWithEd25519KeyAlgorithm- , testCase "sign message convenience API" testSignMessageConvenience- , testCase "typed verify surface matches legacy results" testTypedVerifySurfaceMatchesLegacy- , testCase "strict typed verify rejects tamper" testVerifyMessageStrictRejectsTamper- , testCase "verifySignedMessage convenience API" testVerifySignedMessageConvenience- , testCase "signature generation primitives" testSignaturePrimitives- , testCase- "typed signature payload coercions"- testSignatureDataKindsCoercions- , testCase- "typed armor payload helpers centralize block selection"- testTypedArmorPayloadHelpers- , testCase- "recommendedArmorType infers canonical armor labels from packets"- testRecommendedArmorType- , testCase- "singleClearSignedBlock validates cleartext signature envelopes"- testSingleClearSignedBlock- , testCase- "canonical text signature payload normalization"- testCanonicalTextSigPayloadNormalization- , testCase- "canonical text signatures normalize during signing"- testCanonicalTextSignatureSigningPaths- , testCase- "text normalization modes (RFC9580Strict vs CleartextCompat)"- testTextNormalizationModes- ]- , testGroup- "ASCII armor fixture group"- [ testCase- "v4-encrypted-secret.pgp.aa decodes as an encrypted v4 secret key"- testV4EncryptedSecretArmor- , testCase- "v4-encrypted.rev.aa decodes as a v4 revocation certificate"- testV4EncryptedRevocationArmor- , testCase- "v4-encrypted.rev.aa SigV4 key-revocation semantics"- testV4RevocationSignatureSemantics- , testCase- "v4-encrypted.rev.aa parses as single revocation TKUnknown"- (testRevocationArmorParsesAsSingleTransferableKey "v4-encrypted.rev.aa" True)- , testCase- "v6.rev.aa decodes as a v6 revocation certificate"- (testV6RevocationArmor "v6.rev.aa")- , testCase- "v6.rev.aa SigV6 salt semantics"- (testV6RevocationSignatureSaltSemantics "v6.rev.aa")- , testCase- "v6.rev.aa forbids legacy Issuer key-id subpackets"- (testV6RevocationSignatureRejectsLegacyIssuerKeyID "v6.rev.aa")- , testCase- "v6.rev.aa parses as single revocation TKUnknown"- (testRevocationArmorParsesAsSingleTransferableKey "v6.rev.aa" True)- , testCase- "v6-encrypted.rev.aa decodes as a v6 revocation certificate"- (testV6RevocationArmor "v6-encrypted.rev.aa")- , testCase- "v6-encrypted.rev.aa SigV6 salt semantics"- (testV6RevocationSignatureSaltSemantics "v6-encrypted.rev.aa")- , testCase- "v6-encrypted.rev.aa forbids legacy Issuer key-id subpackets"- (testV6RevocationSignatureRejectsLegacyIssuerKeyID "v6-encrypted.rev.aa")- , testCase- "v6-encrypted.rev.aa parses as single revocation TKUnknown"- (testRevocationArmorParsesAsSingleTransferableKey "v6-encrypted.rev.aa" True)- , testCase- "msg1.asc decodes as a parseable armored message"- testMsg1ArmorFixture- ]- , testGroup- "timeline-aware group"- [ testCase- "timeline-aware primary soft revocation keeps pre-revocation signatures"- testSignerTimelineSoftPrimaryRevocation- , testCase- "timeline-aware primary hard revocation rejects historical signatures"- testSignerTimelineHardPrimaryRevocation- , testCase- "timeline-aware no-reason primary revocation rejects historical signatures"- testSignerTimelineNoReasonPrimaryRevocation- , testCase- "timeline-aware unknown-reason primary revocation rejects historical signatures"- testSignerTimelineUnknownReasonPrimaryRevocation- , testCase- "timeline-aware non-compromise primary revocation stays historical until effective"- testSignerTimelineNonCompromisePrimaryRevocationIsHistorical- , testCase- "timeline-aware temporary primary revocation expires and restores validity"- testSignerTimelineTemporaryPrimaryRevocationExpires- , testCase- "timeline-aware subkey revocation distinguishes pre/post signatures"- testSignerTimelineSubkeyRevocation- ]- ]--testV4EncryptedSecretArmor :: Assertion-testV4EncryptedSecretArmor = do- armors <- loadArmor "v4-encrypted-secret.pgp.aa"- armor <-- case armors of- [a] -> pure a- _ ->- assertFailure "v4 encrypted secret fixture should contain one armored payload" >>- fail "expected one armored payload"- (headers, payload) <-- case armor of- Armor ArmorPrivateKeyBlock hs p -> pure (hs, p)- Armor atype _ _ ->- assertFailure- ("v4 encrypted secret fixture should decode as a private key block, got " ++- show atype) >>- fail "expected private key block"- _ ->- assertFailure "v4 encrypted secret fixture should decode as an armored payload" >>- fail "expected armored payload"- assertEqual- "v4 encrypted secret fixture should keep identifying comments"- [ ("Comment", "D2E0 81E9 3FDC A2E7 8B5F C433 811D 9243 394B 79C1")- , ("Comment", "<v4test@example.org>")- , ("Comment", "v4 Test User")- ]- headers- let packets = parsePkts payload- userIds = [u | UserIdPkt u <- packets]- primarySecretKeys = [(pkp, ska) | SecretKeyPkt pkp ska <- packets]- secretSubkeys = [(pkp, ska) | SecretSubkeyPkt pkp ska <- packets]- assertEqual- "v4 encrypted secret fixture should include expected user IDs"- ["<v4test@example.org>", "v4 Test User"]- userIds- assertEqual- "v4 encrypted secret fixture should contain one primary secret key packet"- 1- (length primarySecretKeys)- assertEqual- "v4 encrypted secret fixture should contain three encrypted secret subkeys"- 3- (length secretSubkeys)- case packets of- (SecretKeyPkt pkp ska:_) -> do- assertEqual- "v4 encrypted secret fixture should contain a v4 primary key"- V4- (_keyVersion pkp)- assertEqual- "v4 encrypted secret fixture should have the expected primary-key fingerprint"- (fp "D2E0 81E9 3FDC A2E7 8B5F C433 811D 9243 394B 79C1")- (fingerprint pkp)- assertEqual- "v4 encrypted secret fixture should use EdDSA for its primary key"- EdDSA- (_pkalgo pkp)- assertEncryptedS2K "primary key" ska- mapM_ (assertEncryptedS2K "subkey" . snd) secretSubkeys- assertEqual- "v4 encrypted secret fixture should contain two EdDSA subkeys and one ECDH subkey"- [EdDSA, EdDSA, ECDH]- (map (_pkalgo . fst) secretSubkeys)- _ -> assertFailure "v4 encrypted secret fixture should start with a secret key packet"- where- assertEncryptedS2K :: String -> SKAddendum -> Assertion- assertEncryptedS2K testLabel ska =- case ska of- SUSSHA1 AES256 (IteratedSalted SHA256 _ iter) _ encryptedPayload -> do- assertEqual- (testLabel ++ " should use expected S2K iteration count")- (IterationCount 65011712)- iter- if BL.null encryptedPayload- then assertFailure (testLabel ++ " should have non-empty encrypted key material")- else pure ()- SUUnencrypted _ _ ->- assertFailure (testLabel ++ " should be encrypted, got unencrypted secret material")- _ ->- assertFailure- (testLabel ++ " should be encrypted with SUSSHA1/AES256/IteratedSalted SHA256")--testV4EncryptedRevocationArmor :: Assertion-testV4EncryptedRevocationArmor = do- armors <- loadArmor "v4-encrypted.rev.aa"- armor <-- case armors of- [a] -> pure a- _ ->- assertFailure "v4 encrypted revocation fixture should contain one armored payload" >>- fail "expected one armored payload"- (headers, payload) <-- case armor of- Armor ArmorPublicKeyBlock hs p -> pure (hs, p)- Armor atype _ _ ->- assertFailure- ("v4 encrypted revocation fixture should decode as a public key block, got " ++- show atype) >>- fail "expected public key block"- _ ->- assertFailure- "v4 encrypted revocation fixture should decode as an armored payload" >>- fail "expected armored payload"- assertEqual- "v4 encrypted revocation fixture should keep identifying comments"- [ ("Comment", "Revocation certificate for")- , ("Comment", "D2E0 81E9 3FDC A2E7 8B5F C433 811D 9243 394B 79C1")- , ("Comment", "<v4test@example.org>")- , ("Comment", "v4 Test User")- ]- headers- let packets = parsePkts payload- case packets of- [PublicKeyPkt pkp, SignaturePkt _] -> do- assertEqual- "v4 encrypted revocation fixture should contain a v4 public key"- V4- (_keyVersion pkp)- assertEqual- "v4 encrypted revocation fixture should contain the expected public key fingerprint"- (fp "D2E0 81E9 3FDC A2E7 8B5F C433 811D 9243 394B 79C1")- (fingerprint pkp)- assertEqual- "v4 encrypted revocation fixture should use EdDSA for its public key"- EdDSA- (_pkalgo pkp)- _ ->- assertFailure- ("v4 encrypted revocation fixture should contain [PublicKeyPkt, SignaturePkt], got: " ++- show packets)--testV6RevocationArmor :: FilePath -> Assertion-testV6RevocationArmor fixture = do- armors <- loadArmor fixture- armor <-- case armors of- [a] -> pure a- _ ->- assertFailure (fixture ++ " fixture should contain one armored payload") >>- fail "expected one armored payload"- payload <-- case armor of- Armor ArmorPublicKeyBlock _ p -> pure p- Armor atype _ _ ->- assertFailure- (fixture ++ " fixture should decode as a public key block, got " ++ show atype) >>- fail "expected public key block"- _ ->- assertFailure (fixture ++ " fixture should decode as an armored payload") >>- fail "expected armored payload"- let packets = parsePkts payload- case packets of- [PublicKeyPkt pkp, SignaturePkt sig] -> do- assertEqual- (fixture ++ " should contain a v6 public key")- V6- (_keyVersion pkp)- case sig of- SigV6 KeyRevocationSig _ _ _ hashed unhashed _ _ -> do- let issuerFps =- [ ifp- | SigSubPacket _ (IssuerFingerprint IssuerFingerprintV6 ifp) <- hashed ++ unhashed- ]- if fingerprint pkp `elem` issuerFps- then pure ()- else- assertFailure- (fixture ++ " should include an IssuerFingerprint v6 matching the public key")- _ ->- assertFailure- (fixture ++ " should contain a SigV6 key-revocation signature")- _ ->- assertFailure- (fixture ++ " should contain [PublicKeyPkt, SignaturePkt], got: " ++- show packets)--testV6RevocationSignatureSaltSemantics :: FilePath -> Assertion-testV6RevocationSignatureSaltSemantics fixture = do- armors <- loadArmor fixture- payload <-- case armors of- [Armor ArmorPublicKeyBlock _ p] -> pure p- _ ->- assertFailure- (fixture ++ " should contain one armored public-key payload") >>- fail "expected one armored payload"- let packets = parsePkts payload- (pkp, sig) <-- case packets of- [PublicKeyPkt pk, SignaturePkt sigV6@(SigV6 _ _ _ _ _ _ _ _)] -> pure (pk, sigV6)- _ ->- assertFailure- (fixture ++ " should parse as [PublicKeyPkt, SignaturePkt SigV6], got " ++ show packets) >>- fail "unexpected revocation fixture packet shape"- case sig of- SigV6 _ _ ha salt _ _ _ _ ->- case expectedV6SaltSizeForTest ha of- Nothing ->- assertFailure- (fixture ++ " uses unsupported v6 signature salt hash algorithm: " ++ show ha)- Just expected ->- assertEqual- (fixture ++ " SigV6 salt size should match hash algorithm")- expected- (fromIntegral (BL.length (unSignatureSalt salt)))- other ->- assertFailure- (fixture ++ " expected SigV6 revocation signature payload, got: " ++ show other)- assertBool- (fixture ++ " should include IssuerFingerprint v6 for the revoked key")- (signatureHasIssuerFingerprintV6 (fingerprint pkp) sig)--testV4RevocationSignatureSemantics :: Assertion-testV4RevocationSignatureSemantics = do- armors <- loadArmor "v4-encrypted.rev.aa"- payload <-- case armors of- [Armor ArmorPublicKeyBlock _ p] -> pure p- _ ->- assertFailure- "v4-encrypted.rev.aa should contain one armored public-key payload" >>- fail "expected one armored payload"- let packets = parsePkts payload- case packets of- [PublicKeyPkt _, SignaturePkt (SigV4 KeyRevocationSig _ _ hashed unhashed _ _)] -> do- assertBool- "v4-encrypted.rev.aa key-revocation signature should include an Issuer key-id subpacket"- (any isIssuerSubpacket (hashed ++ unhashed))- _ ->- assertFailure- ("v4-encrypted.rev.aa should parse as [PublicKeyPkt, SignaturePkt SigV4 KeyRevocationSig], got " ++- show packets)- where- isIssuerSubpacket (SigSubPacket _ Issuer {}) = True- isIssuerSubpacket _ = False--testV6RevocationSignatureRejectsLegacyIssuerKeyID :: FilePath -> Assertion-testV6RevocationSignatureRejectsLegacyIssuerKeyID fixture = do- armors <- loadArmor fixture- payload <-- case armors of- [Armor ArmorPublicKeyBlock _ p] -> pure p- _ ->- assertFailure- (fixture ++ " should contain one armored public-key payload") >>- fail "expected one armored payload"- let packets = parsePkts payload- case packets of- [PublicKeyPkt _, SignaturePkt (SigV6 KeyRevocationSig _ _ _ hashed unhashed _ _)] ->- assertBool- (fixture ++ " SigV6 key-revocation signature must not include Issuer key-id subpackets")- (all (not . isIssuerSubpacket) (hashed ++ unhashed))- _ ->- assertFailure- (fixture ++- " should parse as [PublicKeyPkt, SignaturePkt SigV6 KeyRevocationSig]")- where- isIssuerSubpacket (SigSubPacket _ Issuer {}) = True- isIssuerSubpacket _ = False--testRevocationArmorParsesAsSingleTransferableKey :: FilePath -> Bool -> Assertion-testRevocationArmorParsesAsSingleTransferableKey fixture expectDirectRevs = do- armors <- loadArmor fixture- payload <-- case armors of- [Armor ArmorPublicKeyBlock _ p] -> pure p- _ ->- assertFailure- (fixture ++ " should contain one armored public-key payload") >>- fail "expected one armored payload"- let tks = parseUnknownTKs True (parsePkts payload)- case tks of- [tk] -> do- if expectDirectRevs- then- assertBool- (fixture ++ " transferable key should contain at least one direct-key revocation signature")- (not (null (_tkuRevs tk)))- else pure ()- assertEqual- (fixture ++ " revocation certificate should not carry user IDs")- []- (_tkuUIDs tk)- assertEqual- (fixture ++ " revocation certificate should not carry user attributes")- []- (_tkuUAts tk)- assertEqual- (fixture ++ " revocation certificate should not carry subkeys")- []- (_tkuSubs tk)- _ ->- assertFailure- (fixture ++ " should parse into exactly one transferable key")--testMsg1ArmorFixture :: Assertion-testMsg1ArmorFixture = do- armors <- loadArmor "msg1.asc"- assertBool "msg1.asc should decode to at least one armor block" (not (null armors))- let packetBlocks =- [ parsePkts payload- | Armor _ _ payload <- armors- ]- assertBool- "msg1.asc should contain at least one parseable packet block"- (any (not . null) packetBlocks)--expectedV6SaltSizeForTest :: HashAlgorithm -> Maybe Int-expectedV6SaltSizeForTest =- fmap fromIntegral . signatureV6SaltSizeForHashAlgorithm--signatureHasIssuerFingerprintV6 :: Fingerprint -> SignaturePayload -> Bool-signatureHasIssuerFingerprintV6 expectedFp (SigV6 _ _ _ _ hashed unhashed _ _) =- expectedFp `elem`- [ ifp- | SigSubPacket _ (IssuerFingerprint IssuerFingerprintV6 ifp) <- hashed ++ unhashed- ]-signatureHasIssuerFingerprintV6 _ _ = False--testDefaultedEncryptDecrypt :: Assertion-testDefaultedEncryptDecrypt = do- let passphrase = mkPassphrase "roundtrip1"- payload = mkClearPayload "hello from a balloon farm on Mars"- encryptedResult = encryptMessageDefault DoNotExposeSessionMaterial passphrase payload- encrypted <-- case encryptedResult of- Left err ->- assertFailure ("encryptMessageDefault failed: " ++ show err) >>- fail "encryptMessageDefault failed"- Right (bs, _) -> pure bs- case parsePkts (encryptedPayloadBytes encrypted) of- [SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 AES256 OCB _ iv esk tag)), SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 _ _)] -> do- assertBool "default SKESK v6 IV should be present" (not (BL.null iv))- assertBool "default SKESK v6 wrapped session key should be present" (not (BL.null esk))- assertEqual "default SKESK v6 tag length" 16 (BL.length tag)- other ->- assertFailure- ("default encryption should emit SKESK v6 + SEIPD v2 packets, got " ++- show other)- decrypted <-- case decryptMessage passphrase encrypted of- Left err ->- assertFailure ("decryptMessage failed: " ++ show err) >> fail "decryptMessage failed"- Right bs -> pure bs- assertEqual "default encrypt/decrypt payload roundtrip" payload decrypted--testExplicitEncryptDecrypt :: Assertion-testExplicitEncryptDecrypt = do- let passphrase = mkPassphrase "roundtrip2"- payload = mkClearPayload "hello from nonsenseville"- s2k = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15- iv = IV "1234567890ABCDEF"- encrypted <-- case- encryptMessage- (RFC9580EncryptMessageOptions- { rfc9580EncryptMessageExposure = DoNotExposeSessionMaterial- , rfc9580EncryptMessageSymmetricAlgorithm = AES128- , rfc9580EncryptMessageS2K = s2k- , rfc9580EncryptMessageIV = iv- })- passphrase- payload of- Left err ->- assertFailure ("micro-managed encryption failed: " ++ show err) >>- fail "encryptMessage failed"- Right (bs, _) -> pure bs- case parsePkts (encryptedPayloadBytes encrypted) of- [SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 AES128 OCB _ packetIv esk tag)), SymEncIntegrityProtectedDataPkt (SEIPD2 AES128 OCB 6 _ _)] -> do- assertBool "explicit SKESK v6 IV should be present" (not (BL.null packetIv))- assertBool "explicit SKESK v6 wrapped session key should be present" (not (BL.null esk))- assertEqual "explicit SKESK v6 tag length" 16 (BL.length tag)- other ->- assertFailure- ("explicit AES encryption should default to SKESK v6 + SEIPD v2 packets, got " ++- show other)- decrypted <-- case decryptMessage passphrase encrypted of- Left err ->- assertFailure ("decryption failed: " ++ show err) >> fail "decryptMessage failed"- Right bs -> pure bs- assertEqual "specific encrypt/decrypt payload roundtrip" payload decrypted--testExplicitEncryptExposesEffectiveSessionMaterial :: Assertion-testExplicitEncryptExposesEffectiveSessionMaterial = do- let passphraseBytes = "roundtrip2-session-material"- passphrase = mkPassphrase passphraseBytes- payload = mkClearPayload "hello from session material town"- sa = AES128- s2k = Argon2 (Salt16 (B.pack [0x40 .. 0x4f])) 1 4 15- iv = IV "ABCDEF1234567890"- (encrypted, recovered) <-- case- encryptMessage- (RFC9580EncryptMessageOptions- { rfc9580EncryptMessageExposure = ExposeSessionMaterial- , rfc9580EncryptMessageSymmetricAlgorithm = sa- , rfc9580EncryptMessageS2K = s2k- , rfc9580EncryptMessageIV = iv- })- passphrase- payload of- Left err ->- assertFailure ("explicit encryption with session material failed: " ++ show err) >>- fail "encryptMessage failed"- Right x -> pure x- expectedSessionKey <-- case keySize sa of- Left err ->- assertFailure ("failed to derive expected key size: " ++ show err) >>- fail "keySize failed"- Right keyLen ->- case string2Key s2k keyLen passphraseBytes of- Left err ->- assertFailure ("failed to derive expected session key: " ++ renderS2KError err) >>- fail "string2Key failed"- Right bs -> pure bs- case recovered of- Just recoveredSessionMaterial -> do- assertEqual "recovered session material algorithm" sa (recoveredSessionAlgorithm recoveredSessionMaterial)- assertEqual- "recovered session material key"- (SessionKey expectedSessionKey)- (recoveredSessionKey recoveredSessionMaterial)- Nothing ->- assertFailure "expected exposed session material"- decrypted <-- case decryptMessage passphrase encrypted of- Left err ->- assertFailure ("decryption failed: " ++ show err) >> fail "decryptMessage failed"- Right bs -> pure bs- assertEqual "session-material exposing encrypt/decrypt payload roundtrip" payload decrypted--testDefaultEncryptDoesNotExposeSessionMaterial :: Assertion-testDefaultEncryptDoesNotExposeSessionMaterial = do- let passphrase = mkPassphrase "roundtrip-default-no-session-material"- payload = mkClearPayload "hello from hidden-session-material town"- (_, recovered) <-- do- let encryptedResult =- encryptMessageDefault DoNotExposeSessionMaterial passphrase payload- case encryptedResult of- Left err ->- assertFailure ("default encryption with exposure mode failed: " ++ show err) >>- fail "encryptMessageDefault failed"- Right x -> pure x- case recovered of- Nothing -> pure ()- Just _ -> assertFailure "default exposure mode should not return session material"--testExplicitAES128AEADEncryptDecrypt :: AEADAlgorithm -> Int -> String -> Assertion-testExplicitAES128AEADEncryptDecrypt aead expectedIvLen label = do- let passphraseBytes = "roundtrip-" <> BL.fromStrict (B.pack (map (fromIntegral . fromEnum) label))- passphrase = mkPassphrase passphraseBytes- payload = mkClearPayload ("hello from " <> BL.fromStrict (B.pack (map (fromIntegral . fromEnum) label)))- s2k = Argon2 (Salt16 (B.pack [0x10 .. 0x1f])) 1 4 15- salt = Salt (B.pack [0x20 .. 0x3f])- block = Block [LiteralDataPkt BinaryData BL.empty 0 (clearPayloadBytes payload)]- packets <-- case encryptSEIPDv2WithSKESKBlock AES128 aead 6 salt s2k passphraseBytes block of- Left err ->- assertFailure ("AES-128 " ++ label ++ " encryption failed: " ++ err) >>- fail "encryptSEIPDv2WithSKESKBlock failed"- Right ps -> pure ps- let encrypted = mkEncryptedPayload (runPut (put (Block packets)))- case parsePkts (encryptedPayloadBytes encrypted) of- [SKESKPkt (SKESKPayloadV6Packet (SKESKPayloadV6 AES128 parsedAead _ iv esk tag)), SymEncIntegrityProtectedDataPkt (SEIPD2 AES128 payloadAead 6 _ _)] -> do- assertEqual ("AES-128 " ++ label ++ " SKESK AEAD") aead parsedAead- assertEqual ("AES-128 " ++ label ++ " payload AEAD") aead payloadAead- assertEqual- ("AES-128 " ++ label ++ " SKESK v6 IV length")- (fromIntegral expectedIvLen)- (BL.length iv)- assertBool ("AES-128 " ++ label ++ " wrapped session key should be present") (not (BL.null esk))- assertEqual ("AES-128 " ++ label ++ " SKESK tag length") 16 (BL.length tag)- other ->- assertFailure- ("AES-128 " ++ label ++ " encryption should emit matching SKESK v6 + SEIPD v2 packets, got " ++- show other)- decrypted <-- case decryptMessage passphrase encrypted of- Left err ->- assertFailure ("AES-128 " ++ label ++ " decryption failed: " ++ show err) >>- fail "decryptMessage failed"- Right bs -> pure bs- assertEqual ("AES-128 " ++ label ++ " payload roundtrip") payload decrypted--testExplicitAES128EAXEncryptDecrypt :: Assertion-testExplicitAES128EAXEncryptDecrypt = do- let passphraseBytes = "roundtrip-EAX"- s2k = Argon2 (Salt16 (B.pack [0x10 .. 0x1f])) 1 4 15- salt = Salt (B.pack [0x20 .. 0x3f])- block = Block [LiteralDataPkt BinaryData BL.empty 0 "hello from EAX"]- case encryptSEIPDv2WithSKESKBlock AES128 EAX 6 salt s2k passphraseBytes block of- Left err- | "EAX is currently unsupported by the crypton AEAD backend" `isInfixOf` err ->- pure ()- | otherwise ->- assertFailure ("expected explicit EAX backend limitation, got: " ++ err)- Right packets ->- assertFailure- ("expected AES-128 EAX encryption to fail explicitly, got packets: " ++ show packets)--testExplicitAES128GCMEncryptDecrypt :: Assertion-testExplicitAES128GCMEncryptDecrypt =- testExplicitAES128AEADEncryptDecrypt GCM 12 "GCM"--testExplicitEncryptRejectsDeprecatedS2KHash :: Assertion-testExplicitEncryptRejectsDeprecatedS2KHash = do- let passphrase = mkPassphrase "roundtrip2b"- payload = mkClearPayload "modern-path deprecated s2k hash rejection"- s2k = Salted SHA1 (Salt8 "12345678")- iv = IV "1234567890ABCDEF"- case- encryptMessage- (RFC9580EncryptMessageOptions- { rfc9580EncryptMessageExposure = DoNotExposeSessionMaterial- , rfc9580EncryptMessageSymmetricAlgorithm = AES128- , rfc9580EncryptMessageS2K = s2k- , rfc9580EncryptMessageIV = iv- })- passphrase- payload of- Left (MessageEncryptError err)- | "deprecated hash algorithm disallowed for modern message generation" `isInfixOf` err ->- pure ()- | otherwise ->- assertFailure ("Expected deprecated modern S2K hash rejection, got: " ++ err)- Left err ->- assertFailure ("Expected MessageEncryptError for deprecated modern S2K hash, got " ++ show err)- Right _ ->- assertFailure "Expected encryptMessage AES128 to reject deprecated SHA1 S2K"--testLegacyFallbackEncryptDecrypt :: Assertion-testLegacyFallbackEncryptDecrypt = do- let passphrase = mkPassphrase "roundtrip3"- payload = mkClearPayload "hello from legacy town"- s2k = Salted SHA1 (Salt8 "12345678")- iv = IV mempty- encrypted <-- case- encryptMessage- (RFC4880EncryptMessageOptions- { rfc4880EncryptMessageExposure = DoNotExposeSessionMaterial- , rfc4880EncryptMessageSymmetricAlgorithm = Plaintext- , rfc4880EncryptMessageS2K = s2k- , rfc4880EncryptMessageIV = iv- })- passphrase- payload of- Left err ->- assertFailure ("legacy fallback encryption failed: " ++ show err) >>- fail "encryptMessage fallback failed"- Right (bs, _) -> pure bs- case parsePkts (encryptedPayloadBytes encrypted) of- [SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 Plaintext _ Nothing)), SymEncIntegrityProtectedDataPkt (SEIPD1 1 _)] -> pure ()- other ->- assertFailure- ("non-AES explicit encryption should use RFC4880 SKESKv4 + SEIPDv1 packets, got " ++- show other)- decrypted <-- case decryptMessage passphrase encrypted of- Left err ->- assertFailure ("legacy fallback decryption failed: " ++ show err) >>- fail "decryptMessage fallback failed"- Right bs -> pure bs- assertEqual "legacy fallback encrypt/decrypt payload roundtrip" payload decrypted--testRFC4880EncryptMessageSEIPDv1ParsesCleanly :: Assertion-testRFC4880EncryptMessageSEIPDv1ParsesCleanly = do- let passphraseBytes = "legacy-clean-parse" :: BL.ByteString- passphrase = mkPassphrase passphraseBytes- payload = mkClearPayload "hello from clean legacy town"- s2k = IteratedSalted SHA256 (Salt8 "saltxyz!") (IterationCount 65536)- iv = IV "1234567890ABCDEF"- encrypted <-- case- encryptMessage- (RFC4880EncryptMessageOptions- { rfc4880EncryptMessageExposure = DoNotExposeSessionMaterial- , rfc4880EncryptMessageSymmetricAlgorithm = AES128- , rfc4880EncryptMessageS2K = s2k- , rfc4880EncryptMessageIV = iv- })- passphrase- payload of- Left err ->- assertFailure ("RFC4880 encryption failed: " ++ show err) >>- fail "encryptMessage failed"- Right (bs, _) -> pure bs- packets <-- case parsePktsEither (encryptedPayloadBytes encrypted) of- Left err ->- assertFailure ("encrypted packet parse failed: " ++ show err) >>- fail "parsePktsEither failed"- Right ps -> pure ps- ciphertext <-- case packets of- [SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 parsedSA parsedS2K Nothing)), SymEncIntegrityProtectedDataPkt (SEIPD1 1 payloadBytes)] -> do- assertEqual "RFC4880 encrypted SKESK algorithm" AES128 parsedSA- assertEqual "RFC4880 encrypted S2K" s2k parsedS2K- pure (BL.toStrict payloadBytes)- other ->- assertFailure- ("RFC4880 encryption should emit SKESKv4 + SEIPDv1 packets, got " ++- show other) >>- fail "unexpected encrypted packet layout"- keyLen <-- case keySize AES128 of- Left err ->- assertFailure ("keySize failed for AES128: " ++ show err) >>- fail "keySize failed"- Right n -> pure n- sessionKey <-- case string2Key s2k keyLen passphraseBytes of- Left err ->- assertFailure ("string2Key failed: " ++ renderS2KError err) >>- fail "string2Key failed"- Right keyBytes -> pure keyBytes- (nonce, decrypted) <-- case decryptPreservingNonce AES128 ciphertext sessionKey of- Left err ->- assertFailure ("decryptPreservingNonce failed: " ++ show err) >>- fail "decryptPreservingNonce failed"- Right out -> pure out- cleartext <-- case validateSEIPD1MDC nonce decrypted of- Left err ->- assertFailure ("validateSEIPD1MDC failed: " ++ err) >>- fail "validateSEIPD1MDC failed"- Right out -> pure out- case parsePktsEither (BL.fromStrict cleartext) of- Right [LiteralDataPkt BinaryData filename timestamp clearPayload] -> do- assertEqual "RFC4880 decrypted literal filename" BL.empty filename- assertEqual "RFC4880 decrypted literal timestamp" 0 timestamp- assertEqual "RFC4880 decrypted literal payload" (clearPayloadBytes payload) clearPayload- Right other ->- assertFailure- ("RFC4880 decrypted cleartext should contain exactly one literal packet, got " ++- show other)- Left err ->- assertFailure- ("RFC4880 decrypted cleartext should parse without trailing junk, got " ++- show err)--testDecryptMessageTypedParseFailure :: Assertion-testDecryptMessageTypedParseFailure = do- let passphrase = mkPassphrase "unused"- encrypted = mkEncryptedPayload BL.empty- case decryptMessage passphrase encrypted of- Left (MessageParseFailureError MissingEncryptedMessage) -> pure ()- Left err ->- assertFailure ("Expected MissingEncryptedMessage parse failure, got " ++ show err)- Right clear ->- assertFailure ("Expected parse failure, got payload " ++ show clear)--testDecryptMessageRejectsUnknownCriticalPacket :: Assertion-testDecryptMessageRejectsUnknownCriticalPacket = do- let passphrase = mkPassphrase "unused"- encrypted =- mkEncryptedPayload . runPut . put $- Block [OtherPacketPkt 39 "unknown-critical"]- case decryptMessage passphrase encrypted of- Left (MessageParseFailureError (UnknownCriticalPacketType 39)) -> pure ()- Left err ->- assertFailure- ("Expected UnknownCriticalPacketType 39 parse failure, got " ++ show err)- Right clear ->- assertFailure- ("Expected unknown critical packet rejection, got payload " ++ show clear)--testDecryptMessageTypedDecryptFailure :: Assertion-testDecryptMessageTypedDecryptFailure = do- let correctPassphrase = mkPassphrase "correct passphrase"- wrongPassphrase = mkPassphrase "wrong passphrase"- payload = mkClearPayload "typed decrypt failure payload"- encryptedResult =- encryptMessageDefault DoNotExposeSessionMaterial correctPassphrase payload- encrypted <-- case encryptedResult of- Left err ->- assertFailure ("encryptMessageDefault failed: " ++ show err) >>- fail "encryptMessageDefault failed"- Right (bs, _) -> pure bs- case decryptMessage wrongPassphrase encrypted of- Left (MessageDecryptFailureError (PayloadDecryptFailed _)) -> pure ()- Left (MessageDecryptFailureError (SessionMaterialDerivationFailed _)) -> pure ()- Left err ->- assertFailure ("Expected typed decrypt failure, got " ++ show err)- Right clear ->- assertFailure ("Expected decrypt failure, got payload " ++ show clear)--testDecryptMessageSEIPDv1MDCTampering :: Assertion-testDecryptMessageSEIPDv1MDCTampering = do- let passphrase = mkPassphrase "mdc-tamper-test"- payload = mkClearPayload "payload for MDC tampering test"- s2k = IteratedSalted SHA256 (Salt8 "saltxyz!") (IterationCount 65536)- iv = IV "1234567890ABCDEF"- encrypted <-- case encryptMessage- (RFC4880EncryptMessageOptions- { rfc4880EncryptMessageExposure = DoNotExposeSessionMaterial- , rfc4880EncryptMessageSymmetricAlgorithm = AES128- , rfc4880EncryptMessageS2K = s2k- , rfc4880EncryptMessageIV = iv- })- passphrase- payload of- Left err ->- assertFailure ("SEIPDv1 encryption failed: " ++ show err) >>- fail "encryptMessage failed"- Right (bs, _) -> pure bs- let raw = encryptedPayloadBytes encrypted- midpoint = BL.length raw `div` 2- tampered = BL.take midpoint raw <>- BL.cons (BL.head (BL.drop midpoint raw) `xor` 0xFF)- (BL.drop (midpoint + 1) raw)- case decryptMessage passphrase (mkEncryptedPayload tampered) of- Left (MessageDecryptFailureError (PayloadDecryptFailed msg))- | "MDC" `isInfixOf` msg -> pure ()- | otherwise ->- assertFailure- ("Expected MDC-related PayloadDecryptFailed, got: " ++ msg)- Left err ->- assertFailure- ("Expected PayloadDecryptFailed with MDC error, got: " ++ show err)- Right _ ->- assertFailure "Expected MDC tampering rejection, but decryption succeeded"--testSignMessageShape :: Assertion-testSignMessageShape = do- (signer, signingKey) <- loadUnencryptedRsaSigner- signerV4 <- expectV4PKPayload "RSA v4 signer" signer- signedResult <- signMessageWith (mkRSASignerV4 signerV4 signingKey) (mkClearPayload "message-api signing payload")- signedMessage <-- case signedResult of- Left err ->- assertFailure ("message signing failed: " ++ show err) >> pure mempty- Right bs -> pure bs- case parsePkts signedMessage of- [LiteralDataPkt {}, SignaturePkt _] -> pure ()- _ -> assertFailure "signing output should contain literal data and one signature packet"--testSignMessageRSAV6 :: Assertion-testSignMessageRSAV6 = do- (signer, signingKey) <- loadUnencryptedRsaSignerV6- signerV6 <- expectV6PKPayload "RSA v6 signer" signer- let payload = "message-api signing payload with RSA SigV6"- signedResult <- signMessageWith (mkRSASignerV6 signerV6 signingKey) (mkClearPayload payload)- signedMessage <-- case signedResult of- Left err ->- assertFailure ("RSA SigV6 message signing failed: " ++ show err) >> pure mempty- Right bs -> pure bs- signaturePkt <-- case parsePkts signedMessage of- [LiteralDataPkt {}, sig@(SignaturePkt (SigV6 BinarySig RSA SHA512 salt _ _ _ _))] -> do- assertEqual "SigV6 RSA salt must be 32 bytes for SHA512" 32 (BL.length (unSignatureSalt salt))- pure sig- other ->- assertFailure- ("RSA SigV6 signing output should contain [LiteralDataPkt, RSA SigV6], got " ++- show other) >>- fail "unexpected RSA SigV6 signMessage output shape"- let state = emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}- keyring = [TKUnknown (signer, Nothing) [] [] [] []]- case verifySigWith (verifyAgainstKeys keyring) signaturePkt state Nothing of- Left err ->- assertFailure- ("RSA SigV6 signed message verification failed: " ++- renderVerificationError err)- Right _ -> pure ()--testSignMessageEd25519 :: Assertion-testSignMessageEd25519 = do- (signer, signingKey) <- loadDeterministicEd25519Signer- signerV4 <- expectV4PKPayload "Ed25519 v4 signer" signer- let payload = "message-api signing payload with Ed25519"- signedResult <- signMessageWith (mkEd25519SignerV4 signerV4 signingKey) (mkClearPayload payload)- signedMessage <-- case signedResult of- Left err ->- assertFailure ("Ed25519 message signing failed: " ++ show err) >> pure mempty- Right bs -> pure bs- signaturePkt <-- case parsePkts signedMessage of- [LiteralDataPkt {}, sig@(SignaturePkt (SigV4 BinarySig PKA.Ed25519 SHA512 _ _ _ _))] ->- pure sig- other ->- assertFailure- ("Ed25519 signing output should contain [LiteralDataPkt, Ed25519 SigV4], got " ++- show other) >>- fail "unexpected Ed25519 signMessage output shape"- let state = emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}- keyring = [TKUnknown (signer, Nothing) [] [] [] []]- case verifySigWith (verifyAgainstKeys keyring) signaturePkt state Nothing of- Left err ->- assertFailure- ("Ed25519 signed message verification failed: " ++- renderVerificationError err)- Right _ -> pure ()--testSignMessageEd25519V6 :: Assertion-testSignMessageEd25519V6 = do- (signer, signingKey) <- loadDeterministicEd25519SignerV6- signerV6 <- expectV6PKPayload "Ed25519 v6 signer" signer- let payload = "message-api signing payload with Ed25519 SigV6"- signedResult <- signMessageWith (mkEd25519SignerV6 signerV6 signingKey) (mkClearPayload payload)- signedMessage <-- case signedResult of- Left err ->- assertFailure ("Ed25519 SigV6 message signing failed: " ++ show err) >> pure mempty- Right bs -> pure bs- signaturePkt <-- case parsePkts signedMessage of- [LiteralDataPkt {}, sig@(SignaturePkt (SigV6 BinarySig PKA.Ed25519 SHA512 salt _ _ _ _))] -> do- assertEqual "SigV6 Ed25519 salt must be 32 bytes for SHA512" 32 (BL.length (unSignatureSalt salt))- pure sig- other ->- assertFailure- ("Ed25519 SigV6 signing output should contain [LiteralDataPkt, Ed25519 SigV6], got " ++- show other) >>- fail "unexpected Ed25519 SigV6 signMessage output shape"- case signaturePkt of- SignaturePkt (SigV6 BinarySig PKA.Ed25519 SHA512 _ _ _ _ _) -> do- let state = emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}- keyring = [TKUnknown (signer, Nothing) [] [] [] []]- case verifySigWith (verifyAgainstKeys keyring) signaturePkt state Nothing of- Left err ->- assertFailure- ("Ed25519 SigV6 signed message verification failed: " ++- renderVerificationError err)- Right _ -> pure ()- _ -> assertFailure "expected an Ed25519 SigV6 signature packet"--testV4Ed25519LegacyKeyRejectsMissingPrefix :: Assertion-testV4Ed25519LegacyKeyRejectsMissingPrefix = do- let legacyEd25519Oid = B.pack [0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01]- rawEd25519Public = B.replicate 32 0x01- encoded =- runPut $ do- putWord8 4- putWord32be 0- putWord8 (fromFVal EdDSA)- putWord8 (fromIntegral (B.length legacyEd25519Oid))- putByteString legacyEd25519Oid- putWord16be 256- putByteString rawEd25519Public- case runGetOrFail (get :: Get SomePKPayload) encoded of- Left (_, _, err) ->- assertBool- ("expected invalid-legacy-point parse failure, got: " ++ err)- ("invalid Ed25519Legacy public key" `isInfixOf` err)- Right _ ->- assertFailure "legacy Ed25519 key without 0x40 prefix should be rejected"--testSignMessageEd448 :: Assertion-testSignMessageEd448 = do- (signer, signingKey) <- loadDeterministicEd448Signer- signerV4 <- expectV4PKPayload "Ed448 v4 signer" signer- let payload = "message-api signing payload with Ed448"- signedResult <- signMessageWith (mkEd448SignerV4 signerV4 signingKey) (mkClearPayload payload)- signedMessage <-- case signedResult of- Left err ->- assertFailure ("Ed448 message signing failed: " ++ show err) >> pure mempty- Right bs -> pure bs- signaturePkt <-- case parsePkts signedMessage of- [LiteralDataPkt {}, sig@(SignaturePkt (SigV4 BinarySig PKA.Ed448 SHA512 _ _ _ _))] ->- pure sig- other ->- assertFailure- ("Ed448 signing output should contain [LiteralDataPkt, Ed448 SigV4], got " ++- show other) >>- fail "unexpected Ed448 signMessage output shape"- let state = emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}- keyring = [TKUnknown (signer, Nothing) [] [] [] []]- case verifySigWith (verifyAgainstKeys keyring) signaturePkt state Nothing of- Left err ->- assertFailure- ("Ed448 signed message verification failed: " ++- renderVerificationError err)- Right _ -> pure ()--testSignMessageEd448V6 :: Assertion-testSignMessageEd448V6 = do- (signer, signingKey) <- loadDeterministicEd448SignerV6- signerV6 <- expectV6PKPayload "Ed448 v6 signer" signer- let payload = "message-api signing payload with Ed448 SigV6"- signedResult <- signMessageWith (mkEd448SignerV6 signerV6 signingKey) (mkClearPayload payload)- signedMessage <-- case signedResult of- Left err ->- assertFailure ("Ed448 SigV6 message signing failed: " ++ show err) >> pure mempty- Right bs -> pure bs- signaturePkt <-- case parsePkts signedMessage of- [LiteralDataPkt {}, sig@(SignaturePkt (SigV6 BinarySig PKA.Ed448 SHA512 salt _ _ _ _))] -> do- assertEqual "SigV6 Ed448 salt must be 32 bytes for SHA512" 32 (BL.length (unSignatureSalt salt))- pure sig- other ->- assertFailure- ("Ed448 SigV6 signing output should contain [LiteralDataPkt, Ed448 SigV6], got " ++- show other) >>- fail "unexpected Ed448 SigV6 signMessage output shape"- case signaturePkt of- SignaturePkt (SigV6 BinarySig PKA.Ed448 SHA512 _ _ _ _ _) -> pure ()- _ -> assertFailure "expected an Ed448 SigV6 signature packet"--testV4EdSignaturesUseNativeFixedWidthEncoding :: Assertion-testV4EdSignaturesUseNativeFixedWidthEncoding = do- (_, ed25519SigningKey) <- loadDeterministicEd25519Signer- ed25519Sig <-- case signDataWithEd25519 BinarySig ed25519SigningKey [] [] "v4 ed25519 encoding" of- Left err ->- assertFailure ("Ed25519 v4 signing failed: " ++ renderSignError err) >>- fail "expected Ed25519 signature"- Right sig -> pure sig- case extractV4SignatureAlgorithmFields (runPut (put ed25519Sig)) of- Left err ->- assertFailure ("failed to decode serialized Ed25519 v4 signature payload: " ++ err)- Right (pka, algorithmFields) -> do- assertEqual "Ed25519 v4 signature packet algorithm id" PKA.Ed25519 pka- assertEqual "Ed25519 v4 algorithm field width" 64 (B.length algorithmFields)-- (_, ed448SigningKey) <- loadDeterministicEd448Signer- ed448Sig <-- case signDataWithEd448 BinarySig ed448SigningKey [] [] "v4 ed448 encoding" of- Left err ->- assertFailure ("Ed448 v4 signing failed: " ++ renderSignError err) >>- fail "expected Ed448 signature"- Right sig -> pure sig- case extractV4SignatureAlgorithmFields (runPut (put ed448Sig)) of- Left err ->- assertFailure ("failed to decode serialized Ed448 v4 signature payload: " ++ err)- Right (pka, algorithmFields) -> do- assertEqual "Ed448 v4 signature packet algorithm id" PKA.Ed448 pka- assertEqual "Ed448 v4 algorithm field width" 114 (B.length algorithmFields)--testVerifyDetachedEd25519WithoutIssuerHints :: Assertion-testVerifyDetachedEd25519WithoutIssuerHints = do- (signer, signingKey) <- loadDeterministicEd25519Signer- let payload = "detached v4 eddsa payload without issuer hints"- state = emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}- keyring = mkTestKeyring [TKUnknown (signer, Nothing) [] [] [] []]- sigPayload <-- case signDataWithEd25519 BinarySig signingKey [] [] payload of- Left err ->- assertFailure ("Ed25519 detached signing failed: " ++ renderSignError err) >>- fail (renderSignError err)- Right sig -> pure sig- case verifySigWith (verifyAgainstKeyring keyring) (SignaturePkt sigPayload) state Nothing of- Left err ->- assertFailure- ("Ed25519 detached verification without issuer hints failed: " ++- renderVerificationError err)- Right _ -> pure ()--testVerifyDetachedEd25519WithFakeIssuerHint :: Assertion-testVerifyDetachedEd25519WithFakeIssuerHint = do- (signer, signingKey) <- loadDeterministicEd25519Signer- let payload = "detached v4 eddsa payload with fake issuer hint"- state = emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}- keyring = mkTestKeyring [TKUnknown (signer, Nothing) [] [] [] []]- fakeIssuer = EightOctetKeyId "\x01\x23\x45\x67\x89\xab\xcd\xef"- unhashed = [SigSubPacket False (Issuer fakeIssuer)]- sigPayload <-- case signDataWithEd25519 BinarySig signingKey [] unhashed payload of- Left err ->- assertFailure ("Ed25519 detached signing failed: " ++ renderSignError err) >>- fail (renderSignError err)- Right sig -> pure sig- case verifySigWith (verifyAgainstKeyring keyring) (SignaturePkt sigPayload) state Nothing of- Left err ->- assertFailure- ("Ed25519 detached verification with fake issuer hint failed: " ++- renderVerificationError err)- Right _ -> pure ()--testVerifyDetachedEd25519WithoutIssuerHintsAgainstKeys :: Assertion-testVerifyDetachedEd25519WithoutIssuerHintsAgainstKeys = do- (signer, signingKey) <- loadDeterministicEd25519Signer- let payload = "detached v4 eddsa payload without issuer hints (verifyAgainstKeys)"- state = emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}- keys = [TKUnknown (signer, Nothing) [] [] [] []]- sigPayload <-- case signDataWithEd25519 BinarySig signingKey [] [] payload of- Left err ->- assertFailure ("Ed25519 detached signing failed: " ++ renderSignError err) >>- fail (renderSignError err)- Right sig -> pure sig- case verifySigWith (verifyAgainstKeys keys) (SignaturePkt sigPayload) state Nothing of- Left err ->- assertFailure- ("Ed25519 detached verification against keys without issuer hints failed: " ++- renderVerificationError err)- Right _ -> pure ()--testVerifyDetachedEd25519WithFakeIssuerHintAgainstKeys :: Assertion-testVerifyDetachedEd25519WithFakeIssuerHintAgainstKeys = do- (signer, signingKey) <- loadDeterministicEd25519Signer- let payload = "detached v4 eddsa payload with fake issuer hint (verifyAgainstKeys)"- state = emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}- keys = [TKUnknown (signer, Nothing) [] [] [] []]- fakeIssuer = EightOctetKeyId "\x01\x23\x45\x67\x89\xab\xcd\xef"- unhashed = [SigSubPacket False (Issuer fakeIssuer)]- sigPayload <-- case signDataWithEd25519 BinarySig signingKey [] unhashed payload of- Left err ->- assertFailure ("Ed25519 detached signing failed: " ++ renderSignError err) >>- fail (renderSignError err)- Right sig -> pure sig- case verifySigWith (verifyAgainstKeys keys) (SignaturePkt sigPayload) state Nothing of- Left err ->- assertFailure- ("Ed25519 detached verification against keys with fake issuer hint failed: " ++- renderVerificationError err)- Right _ -> pure ()--testCanonicalTextSigPayloadNormalization :: Assertion-testCanonicalTextSigPayloadNormalization = do- let state =- emptyPSC- { lastLD =- LiteralDataPkt- TextData- BL.empty- (ThirtyTwoBitTimeStamp 0)- "line1 \t\nline2\t \rline3\t \r\nline4 \t"- }- assertEqual- "Binary signatures preserve original line endings"- "line1 \t\nline2\t \rline3\t \r\nline4 \t"- (payloadForSig BinarySig state)- assertEqual- "Canonical text signatures normalize line endings and trim trailing whitespace"- "line1\r\nline2\r\nline3\r\nline4"- (payloadForSig CanonicalTextSig state)--testTextNormalizationModes :: Assertion-testTextNormalizationModes = do- let state =- emptyPSC- { lastLD =- LiteralDataPkt- TextData- BL.empty- (ThirtyTwoBitTimeStamp 0)- "line1 \t\nline2\t \r\nline3"- }- let cleartextResult = payloadForSigWith CleartextCompat CanonicalTextSig state- strictResult = payloadForSigWith RFC9580Strict CanonicalTextSig state- assertEqual- "CleartextCompat mode strips trailing whitespace per line"- "line1\r\nline2\r\nline3"- cleartextResult- assertEqual- "RFC9580Strict mode preserves trailing whitespace (CRLF-only normalization)"- "line1 \t\r\nline2\t \r\nline3"- strictResult- (signer, signingKey) <- loadUnencryptedRsaSigner- issuerKeyId <-- case eightOctetKeyID signer of- Left err ->- assertFailure ("failed to derive issuer key id: " ++ err) >>- fail "expected issuer key id"- Right i -> pure i- let payload = "line with trailing space \nno trailing space\n"- hashed = [SigSubPacket False (IssuerFingerprint IssuerFingerprintV4 (fingerprint signer))]- unhashed = [SigSubPacket False (Issuer issuerKeyId)]- builderCompat = sigBuilderInit @'PKA.RSA CanonicalTextSig SHA512- builderStrict = (sigBuilderInit @'PKA.RSA CanonicalTextSig SHA512) { sbTextNormMode = RFC9580Strict }- withSubs b = addUnhashedSubs (listToUnhashedSubs unhashed) (addHashedSubs (listToHashedSubs hashed) b)- bc = withSubs builderCompat- bs = withSubs builderStrict- sigCompat <- case signDataWithRSABuilder bc signingKey payload of- Left err -> assertFailure ("CleartextCompat sign failed: " ++ renderSignError err) >> fail ""- Right s -> pure s- sigStrict <- case signDataWithRSABuilder bs signingKey payload of- Left err -> assertFailure ("RFC9580Strict sign failed: " ++ renderSignError err) >> fail ""- Right s -> pure s- assertBool- "CleartextCompat and RFC9580Strict produce different signatures for payloads with trailing whitespace"- (sigCompat /= sigStrict)--testCanonicalTextSignatureSigningPaths :: Assertion-testCanonicalTextSignatureSigningPaths = do- (signer, signingKey) <- loadUnencryptedRsaSigner- issuerKeyId <-- case eightOctetKeyID signer of- Left err ->- assertFailure ("failed to derive issuer key id: " ++ err) >>- fail "expected issuer key id"- Right i -> pure i- let mixedPayload = "line1 \t\nline2\t \rline3\t \r\nline4 \t"- normalizedPayload = "line1\r\nline2\r\nline3\r\nline4"- hashed = [SigSubPacket False (IssuerFingerprint IssuerFingerprintV4 (fingerprint signer))]- unhashed = [SigSubPacket False (Issuer issuerKeyId)]- primitiveMixed <-- case signDataWithRSA CanonicalTextSig signingKey hashed unhashed mixedPayload of- Left err ->- assertFailure ("canonical text primitive signing failed: " ++ renderSignError err) >>- fail (renderSignError err)- Right sig -> pure sig- primitiveNormalized <-- case signDataWithRSA CanonicalTextSig signingKey hashed unhashed normalizedPayload of- Left err ->- assertFailure ("canonical text primitive signing (normalized payload) failed: " ++ renderSignError err) >>- fail (renderSignError err)- Right sig -> pure sig- assertEqual- "primitive canonical text signing should normalize mixed line endings"- primitiveNormalized- primitiveMixed-- let builder = sigBuilderInit @'PKA.RSA CanonicalTextSig SHA512- withHashed = addHashedSubs (listToHashedSubs hashed) builder- withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed- builderMixed <-- case signDataWithRSABuilder withUnhashed signingKey mixedPayload of- Left err ->- assertFailure ("canonical text builder signing failed: " ++ renderSignError err) >>- fail (renderSignError err)- Right sig -> pure sig- builderNormalized <-- case signDataWithRSABuilder withUnhashed signingKey normalizedPayload of- Left err ->- assertFailure ("canonical text builder signing (normalized payload) failed: " ++ renderSignError err) >>- fail (renderSignError err)- Right sig -> pure sig- assertEqual- "builder canonical text signing should normalize mixed line endings"- builderNormalized- builderMixed--testSignaturePrimitives :: Assertion-testSignaturePrimitives = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let userId = UserId "primitive-api@example.org"- assertSigType ::- String -> SigType -> Either SignError SignaturePayload -> Assertion- assertSigType testLabel expected result =- case result of- Left err -> assertFailure (testLabel ++ " failed: " ++ renderSignError err)- Right (SigV4 sigType _ _ _ _ _ _) ->- assertEqual (testLabel ++ " uses expected signature type") expected sigType- Right _ -> assertFailure (testLabel ++ " should generate a V4 signature payload")- assertSigType- "certification signature"- GenericCert- (signCertificationWithRSA GenericCert signer userId [] [] signingKey)- assertSigType- "key revocation signature"- KeyRevocationSig- (signKeyRevocationWithRSA signer [] [] signingKey)- assertSigType- "subkey revocation signature"- SubkeyRevocationSig- (signSubkeyRevocationWithRSA signer signer [] [] signingKey)- assertSigType- "certification revocation signature"- CertRevocationSig- (signCertRevocationWithRSA signer userId [] [] signingKey)- let left16Payload = "left16 primitive payload"- left16Keyring = mkTestKeyring [TKUnknown (signer, Nothing) [] [] [] []]- left16Sig <-- case signDataWithRSA BinarySig signingKey [] [] left16Payload of- Left err ->- assertFailure ("RSA primitive signing for left16 failed: " ++ renderSignError err) >>- fail "expected RSA signature payload"- Right sigPayload -> pure sigPayload- case verifyAgainstKeyring left16Keyring (SignaturePkt left16Sig) Nothing left16Payload of- Right _ -> pure ()- Left err ->- assertFailure- ("fresh RSA signature should verify with matching left16, got: " ++- renderVerificationError err)- let tamperedLeft16Sig =- case left16Sig of- SigV4 st pka ha hs us l16 mpis -> SigV4 st pka ha hs us (l16 + 1) mpis- other -> other- case verifyAgainstKeyring left16Keyring (SignaturePkt tamperedLeft16Sig) Nothing left16Payload of- Left _ -> pure ()- Right _ ->- assertFailure "verification unexpectedly succeeded with tampered left16"- if isRight (signCertificationWithRSA KeyRevocationSig signer userId [] [] signingKey)- then- assertFailure- "certification primitive should reject non-certification signature types"- else pure ()- (edSigner, edSigningKey) <- loadDeterministicEd25519Signer- edIssuerKeyId <-- case eightOctetKeyID edSigner of- Left err ->- assertFailure ("failed to derive Ed25519 issuer key id: " ++ err) >>- fail "expected Ed25519 issuer key id"- Right i -> pure i- let edPayload = "primitive Ed25519 payload"- edHashed = [SigSubPacket False (IssuerFingerprint IssuerFingerprintV4 (fingerprint edSigner))]- edUnhashed = [SigSubPacket False (Issuer edIssuerKeyId)]- case signDataWithEd25519 BinarySig edSigningKey edHashed edUnhashed edPayload of- Left err -> assertFailure ("Ed25519 primitive signing failed: " ++ renderSignError err)- Right (SigV4 BinarySig PKA.Ed25519 SHA512 _ _ _ _) -> pure ()- Right other ->- assertFailure ("Ed25519 primitive should generate an Ed25519 SigV4 payload, got " ++ show other)- let v6Salt = SignatureSalt (BL.replicate 32 0x42)- case signDataWithEd25519V6 BinarySig v6Salt edSigningKey edHashed edUnhashed edPayload of- Left err -> assertFailure ("Ed25519 SigV6 primitive signing failed: " ++ renderSignError err)- Right (SigV6 BinarySig PKA.Ed25519 SHA512 salt _ _ _ _) ->- assertEqual "Ed25519 SigV6 primitive should preserve 32-byte salt" 32 (BL.length (unSignatureSalt salt))- Right other ->- assertFailure ("Ed25519 SigV6 primitive should generate an Ed25519 SigV6 payload, got " ++ show other)- (ed448Signer, ed448SigningKey) <- loadDeterministicEd448Signer- ed448IssuerKeyId <-- case eightOctetKeyID ed448Signer of- Left err ->- assertFailure ("failed to derive Ed448 issuer key id: " ++ err) >>- fail "expected Ed448 issuer key id"- Right i -> pure i- let ed448Payload = "primitive Ed448 payload"- ed448Hashed = [SigSubPacket False (IssuerFingerprint IssuerFingerprintV4 (fingerprint ed448Signer))]- ed448Unhashed = [SigSubPacket False (Issuer ed448IssuerKeyId)]- case signDataWithEd448 BinarySig ed448SigningKey ed448Hashed ed448Unhashed ed448Payload of- Left err -> assertFailure ("Ed448 primitive signing failed: " ++ renderSignError err)- Right (SigV4 BinarySig PKA.Ed448 SHA512 _ _ _ _) -> pure ()- Right other ->- assertFailure ("Ed448 primitive should generate an Ed448 SigV4 payload, got " ++ show other)- case signDataWithEd448V6 BinarySig v6Salt ed448SigningKey ed448Hashed ed448Unhashed ed448Payload of- Left err -> assertFailure ("Ed448 SigV6 primitive signing failed: " ++ renderSignError err)- Right (SigV6 BinarySig PKA.Ed448 SHA512 salt _ _ _ _) ->- assertEqual "Ed448 SigV6 primitive should preserve 32-byte salt" 32 (BL.length (unSignatureSalt salt))- Right other ->- assertFailure ("Ed448 SigV6 primitive should generate an Ed448 SigV6 payload, got " ++ show other)--testSignatureDataKindsCoercions :: Assertion-testSignatureDataKindsCoercions = do- let sigV3 =- SigV3- BinarySig- (ThirtyTwoBitTimeStamp 1)- (EightOctetKeyId "\x01\x02\x03\x04\x05\x06\x07\x08")- RSA- SHA256- 0- (NE.fromList [MPI 1])- sigV4 =- SigV4- BinarySig- RSA- SHA256- []- []- 0- (NE.fromList [MPI 2])- sigV6 =- SigV6- BinarySig- PKA.Ed25519- SHA512- (SignatureSalt (BL.replicate 32 0x01))- []- []- 0- (NE.fromList [MPI 3, MPI 4])- sigOther = SigVOther 77 "opaque-signature-body"- assertPacketRoundTrip label expected pkt =- case fromPktEitherSomeSignatureV pkt of- Left err -> assertFailure (label ++ " packet coercion failed: " ++ err)- Right (SomeSignatureV typedSig) ->- assertEqual- (label ++ " packet coercion preserves payload")- expected- (signaturePayloadFromSignatureV typedSig)- assertBool- "toSomeSignaturePayload should preserve SigV3 witness"- (isRight (asSignaturePayloadV3 sigV3))- assertBool- "toSomeSignaturePayload should preserve SigV4 witness"- (isRight (asSignaturePayloadV4 sigV4))- assertBool- "toSomeSignaturePayload should preserve SigV6 witness"- (isRight (asSignaturePayloadV6 sigV6))- assertBool- "toSomeSignaturePayload should preserve SigVOther witness"- (isRight (asSignaturePayloadOther sigOther))- case asSignaturePayloadV3 sigV3 of- Left err -> assertFailure ("asSignaturePayloadV3 should accept SigV3: " ++ err)- Right typed ->- assertEqual "asSignaturePayloadV3 round-trips SigV3" sigV3 (toSignaturePayload typed)- case asSignaturePayloadV4 sigV4 of- Left err -> assertFailure ("asSignaturePayloadV4 should accept SigV4: " ++ err)- Right typed ->- assertEqual "asSignaturePayloadV4 round-trips SigV4" sigV4 (toSignaturePayload typed)- case asSignaturePayloadV6 sigV6 of- Left err -> assertFailure ("asSignaturePayloadV6 should accept SigV6: " ++ err)- Right typed ->- assertEqual "asSignaturePayloadV6 round-trips SigV6" sigV6 (toSignaturePayload typed)- case asSignaturePayloadOther sigOther of- Left err -> assertFailure ("asSignaturePayloadOther should accept SigVOther: " ++ err)- Right typed ->- assertEqual "asSignaturePayloadOther round-trips SigVOther" sigOther (toSignaturePayload typed)- assertBool- "asSignaturePayloadV6 rejects SigV4"- (not (isRight (asSignaturePayloadV6 sigV4)))- assertBool- "asSignaturePayloadV4 rejects SigVOther"- (not (isRight (asSignaturePayloadV4 sigOther)))- assertPacketRoundTrip "SigV3" sigV3 (SignaturePkt sigV3)- assertPacketRoundTrip "SigV4" sigV4 (SignaturePkt sigV4)- assertPacketRoundTrip "SigV6" sigV6 (SignaturePkt sigV6)- assertPacketRoundTrip "SigVOther" sigOther (SignaturePkt sigOther)- assertBool- "fromPktEitherSomeSignatureV rejects non-signature packets"- (not (isRight (fromPktEitherSomeSignatureV (LiteralDataPkt BinaryData "" 0 ""))))--testTypedArmorPayloadHelpers :: Assertion-testTypedArmorPayloadHelpers = do- let armors =- [ Armor ArmorPublicKeyBlock [] "public-one"- , Armor ArmorMessage [] "message-one"- , Armor ArmorPublicKeyBlock [] "public-two"- ]- publicPayloads = map BL.toStrict (armorPayloadsOfType ArmorPublicKeyBlock armors)- assertEqual- "armorPayloadsOfType returns all matching typed blocks in order"- ["public-one", "public-two"]- publicPayloads- assertEqual- "singleArmorPayloadOfType returns the sole matching message payload"- (Right "message-one")- (fmap BL.toStrict (singleArmorPayloadOfType ArmorMessage armors))- assertBool- "singleArmorPayloadOfType fails when no typed blocks exist"- (isLeft (singleArmorPayloadOfType ArmorPrivateKeyBlock armors))- assertBool- "singleArmorPayloadOfType fails when multiple typed blocks exist"- (isLeft (singleArmorPayloadOfType ArmorPublicKeyBlock armors))-testRecommendedArmorType :: Assertion-testRecommendedArmorType = do- secretArmors <- loadArmor "v4-encrypted-secret.pgp.aa"- publicArmors <- loadArmor "v4-encrypted.rev.aa"- secretPayload <-- case singleArmorPayloadOfType ArmorPrivateKeyBlock secretArmors of- Left err -> assertFailure err >> fail err- Right payload -> pure payload- publicPayload <-- case singleArmorPayloadOfType ArmorPublicKeyBlock publicArmors of- Left err -> assertFailure err >> fail err- Right payload -> pure payload- let secretPkts = parsePkts secretPayload- publicPkts = parsePkts publicPayload- firstPkt label pkts =- case pkts of- [] -> assertFailure (label ++ " should contain at least one packet") >> fail "missing packet"- pkt:_ -> pure pkt- firstSecret <- firstPkt "secret armor fixture" secretPkts- firstPublic <- firstPkt "public armor fixture" publicPkts- let sig = SignaturePkt (SigV4 BinarySig RSA SHA256 [] [] 0 (NE.fromList [MPI 1]))- assertEqual "recommendedArmorType rejects empty packet streams" Nothing (recommendedArmorType [])- assertEqual- "recommendedArmorType maps secret-key packets to private key armor"- (Just ArmorPrivateKeyBlock)- (recommendedArmorType [firstSecret])- assertEqual- "recommendedArmorType maps public-key packets to public key armor"- (Just ArmorPublicKeyBlock)- (recommendedArmorType [firstPublic])- assertEqual- "recommendedArmorType maps signature packets to signature armor"- (Just ArmorSignature)- (recommendedArmorType [sig])- assertEqual- "recommendedArmorType defaults non-key/signature packets to message armor"- (Just ArmorMessage)- (recommendedArmorType [LiteralDataPkt BinaryData "" 0 "payload"])-testSingleClearSignedBlock :: Assertion-testSingleClearSignedBlock = do- let clearSigned =- ClearSigned- [("Hash", "SHA256")]- "signed cleartext payload\n"- (Armor ArmorSignature [("Version", "test-suite")] "detached-signature")- assertEqual- "singleClearSignedBlock extracts cleartext and signature payloads"- (Right ([("Hash", "SHA256")], "signed cleartext payload\n", "detached-signature"))- (singleClearSignedBlock [clearSigned])- assertBool- "singleClearSignedBlock rejects absent clear-signed blocks"- (isLeft (singleClearSignedBlock [Armor ArmorMessage [] "payload"]))- assertBool- "singleClearSignedBlock rejects multiple clear-signed blocks"- (isLeft (singleClearSignedBlock [clearSigned, clearSigned]))- assertBool- "singleClearSignedBlock rejects non-signature inner armor blocks"- (isLeft (singleClearSignedBlock [ClearSigned [] "payload" (Armor ArmorMessage [] "not-signature")]))-testSignerTimelineSoftPrimaryRevocation :: Assertion-testSignerTimelineSoftPrimaryRevocation = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let payload = "soft primary revocation timeline payload"- keyCreated = _timestamp signer- sigBeforeTime = addTimestampSeconds keyCreated 10- revocationTime = addTimestampSeconds keyCreated 20- sigAfterTime = addTimestampSeconds keyCreated 30- sigBefore <-- signBinaryMessageWithRSAAt signer signingKey sigBeforeTime payload- revocation <-- signKeyRevocationWithReasonAt signer signingKey revocationTime KeySuperseded- sigAfter <-- signBinaryMessageWithRSAAt signer signingKey sigAfterTime payload- let keyring = mkTestKeyring [TKUnknown (signer, Nothing) [revocation] [] [] []]- assertSingleSignerFingerprint- "soft primary revocation should preserve pre-revocation signatures"- (fingerprint signer)- (verifyTimelinePackets keyring payload sigBefore)- assertSingleFailureContainsTimeline- "soft primary revocation should reject post-revocation signatures"- "signing key is revoked"- (verifyTimelinePackets keyring payload sigAfter)--testSignerTimelineHardPrimaryRevocation :: Assertion-testSignerTimelineHardPrimaryRevocation = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let payload = "hard primary revocation timeline payload"- keyCreated = _timestamp signer- sigBeforeTime = addTimestampSeconds keyCreated 10- revocationTime = addTimestampSeconds keyCreated 20- sigBefore <-- signBinaryMessageWithRSAAt signer signingKey sigBeforeTime payload- revocation <-- signKeyRevocationWithReasonAt signer signingKey revocationTime KeyMaterialCompromised- let keyring = mkTestKeyring [TKUnknown (signer, Nothing) [revocation] [] [] []]- assertSingleFailureContainsTimeline- "hard primary revocation should reject even pre-revocation signatures"- "signing key is revoked"- (verifyTimelinePackets keyring payload sigBefore)--testSignerTimelineNoReasonPrimaryRevocation :: Assertion-testSignerTimelineNoReasonPrimaryRevocation = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let payload = "no-reason primary revocation timeline payload"- keyCreated = _timestamp signer- sigBeforeTime = addTimestampSeconds keyCreated 10- revocationTime = addTimestampSeconds keyCreated 20- sigBefore <-- signBinaryMessageWithRSAAt signer signingKey sigBeforeTime payload- revocation <-- signKeyRevocationWithReasonAt signer signingKey revocationTime NoReason- let keyring = mkTestKeyring [TKUnknown (signer, Nothing) [revocation] [] [] []]- assertSingleFailureContainsTimeline- "no-reason primary revocation should reject even pre-revocation signatures"- "signing key is revoked"- (verifyTimelinePackets keyring payload sigBefore)--testSignerTimelineUnknownReasonPrimaryRevocation :: Assertion-testSignerTimelineUnknownReasonPrimaryRevocation = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let payload = "unknown-reason primary revocation timeline payload"- keyCreated = _timestamp signer- sigBeforeTime = addTimestampSeconds keyCreated 10- revocationTime = addTimestampSeconds keyCreated 20- sigBefore <-- signBinaryMessageWithRSAAt signer signingKey sigBeforeTime payload- revocation <-- signKeyRevocationWithReasonAt signer signingKey revocationTime (RCoOther 100)- let keyring = mkTestKeyring [TKUnknown (signer, Nothing) [revocation] [] [] []]- assertSingleFailureContainsTimeline- "unknown-reason primary revocation should reject even pre-revocation signatures"- "signing key is revoked"- (verifyTimelinePackets keyring payload sigBefore)--testSignerTimelineNonCompromisePrimaryRevocationIsHistorical :: Assertion-testSignerTimelineNonCompromisePrimaryRevocationIsHistorical = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let payload = "non-compromise primary revocation timeline payload"- keyCreated = _timestamp signer- sigBeforeTime = addTimestampSeconds keyCreated 10- revocationTime = addTimestampSeconds keyCreated 20- sigAfterTime = addTimestampSeconds keyCreated 30- sigBefore <-- signBinaryMessageWithRSAAt signer signingKey sigBeforeTime payload- revocation <-- signKeyRevocationWithReasonAt- signer- signingKey- revocationTime- UserIdInfoNoLongerValid- sigAfter <-- signBinaryMessageWithRSAAt signer signingKey sigAfterTime payload- let keyring = mkTestKeyring [TKUnknown (signer, Nothing) [revocation] [] [] []]- assertSingleSignerFingerprint- "non-compromise primary revocation should preserve pre-revocation signatures"- (fingerprint signer)- (verifyTimelinePackets keyring payload sigBefore)- assertSingleFailureContainsTimeline- "non-compromise primary revocation should reject post-revocation signatures"- "signing key is revoked"- (verifyTimelinePackets keyring payload sigAfter)--testSignerTimelineTemporaryPrimaryRevocationExpires :: Assertion-testSignerTimelineTemporaryPrimaryRevocationExpires = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let payload = "temporary primary revocation expiry timeline payload"- keyCreated = _timestamp signer- sigBeforeTime = addTimestampSeconds keyCreated 10- revocationTime = addTimestampSeconds keyCreated 20- sigDuringTime = addTimestampSeconds keyCreated 25- sigAfterTime = addTimestampSeconds keyCreated 35- sigBefore <-- signBinaryMessageWithRSAAt signer signingKey sigBeforeTime payload- revocation <-- signKeyRevocationWithReasonAndExtrasAt- signer- signingKey- revocationTime- KeySuperseded- [SigSubPacket False (SigExpirationTime 10)]- sigDuring <-- signBinaryMessageWithRSAAt signer signingKey sigDuringTime payload- sigAfter <-- signBinaryMessageWithRSAAt signer signingKey sigAfterTime payload- let keyring = mkTestKeyring [TKUnknown (signer, Nothing) [revocation] [] [] []]- assertSingleSignerFingerprint- "temporary primary revocation should preserve pre-revocation signatures"- (fingerprint signer)- (verifyTimelinePackets keyring payload sigBefore)- assertSingleFailureContainsTimeline- "temporary primary revocation should reject signatures while revocation is effective"- "signing key is revoked"- (verifyTimelinePackets keyring payload sigDuring)- assertSingleSignerFingerprint- "temporary primary revocation should allow signatures after revocation expiration"- (fingerprint signer)- (verifyTimelinePackets keyring payload sigAfter)--testSignerTimelineSubkeyRevocation :: Assertion-testSignerTimelineSubkeyRevocation = do- (primarySigner, primarySigningKey) <- loadUnencryptedRsaSigner- (subkeySigner, subkeySigningKey) <- loadDeterministicEd25519Signer- let payload = "subkey revocation timeline payload"- keyCreated = _timestamp primarySigner- bindingTime = addTimestampSeconds keyCreated 10- sigBeforeTime = addTimestampSeconds keyCreated 20- revocationTime = addTimestampSeconds keyCreated 30- sigAfterTime = addTimestampSeconds keyCreated 40- subkeyPacket = PublicSubkeyPkt subkeySigner- bindingSig <-- signSubkeyBindingWithRSAAt primarySigner subkeySigner primarySigningKey bindingTime- sigBefore <-- signBinaryMessageWithEd25519At subkeySigner subkeySigningKey sigBeforeTime payload- revocationSig <-- signSubkeyRevocationWithRSAAt primarySigner subkeySigner primarySigningKey revocationTime- sigAfter <-- signBinaryMessageWithEd25519At subkeySigner subkeySigningKey sigAfterTime payload- let keyring =- mkTestKeyring- [TKUnknown (primarySigner, Nothing) [] [] [] [(subkeyPacket, [bindingSig, revocationSig])]]- assertSingleSignerFingerprint- "subkey revocation should preserve pre-revocation signatures"- (fingerprint subkeySigner)- (verifyTimelinePackets keyring payload sigBefore)- assertSingleFailureContainsTimeline- "subkey revocation should reject post-revocation signatures"- "signing key was not valid at the signature creation time"- (verifyTimelinePackets keyring payload sigAfter)--testSignMessageConvenience :: Assertion-testSignMessageConvenience = do- (signer, signingKey) <- loadUnencryptedRsaSigner- signerV4 <- expectV4PKPayload "RSA v4 convenience signer" signer- signedResult <- signMessage (mkRSASignerV4 signerV4 signingKey) "message-api convenience signing payload"- signedMessage <-- case signedResult of- Left err ->- assertFailure ("message signing convenience API failed: " ++ show err) >> pure mempty- Right bs -> pure bs- case parsePkts signedMessage of- [LiteralDataPkt {}, SignaturePkt _] -> pure ()- _ -> assertFailure "convenience signing output should contain literal data and one signature packet"--testTypedVerifySurfaceMatchesLegacy :: Assertion-testTypedVerifySurfaceMatchesLegacy = do- kr <- loadKeyring "pubring.gpg"- signedMessage <- readFixtureLazy "uncompressed-ops-rsa.gpg"- let typedResults =- verifyMessage- defaultVerificationOptions- { verificationPolicy = VerifyInformational- , verificationMode = VerificationStreaming- }- kr- signedMessage- strictResults =- verifyMessage- defaultVerificationOptions- { verificationPolicy = VerifyStrict- , verificationMode = VerificationStreaming- }- kr- signedMessage- normalizeTyped :: Either VerificationError Verification -> Either Bool Fingerprint- normalizeTyped (Left _) = Left False- normalizeTyped (Right v) = Right (fingerprint (_verificationSigner v))- assertEqual- "strict verification should collapse informational failures"- (map normalizeTyped (either (pure . Left) (Right <$>) (sequence typedResults)))- (map normalizeTyped strictResults)- assertBool- "informational verification should emit at least one result for known-good fixture"- (not (null (map normalizeTyped typedResults)))--testVerifyMessageStrictRejectsTamper :: Assertion-testVerifyMessageStrictRejectsTamper = do- kr <- loadKeyring "pubring.gpg"- packets <- loadAndDecompressPkts "uncompressed-ops-rsa.gpg"- let tamperedPackets = map tamperLiteral packets- strictOptions =- defaultVerificationOptions- { verificationPolicy = VerifyStrict- , verificationMode = VerificationStreaming- }- case verifyMessagePackets strictOptions kr tamperedPackets of- [Left _] -> pure ()- _ -> assertFailure "strict typed verification should fail on tampered payload"- where- tamperLiteral (LiteralDataPkt dt fn ts payload) =- LiteralDataPkt dt fn ts (BL.snoc payload 0)- tamperLiteral pkt = pkt--testVerifySignedMessageConvenience :: Assertion-testVerifySignedMessageConvenience = do- kr <- loadKeyring "pubring.gpg"- signedMessage <- readFixtureLazy "uncompressed-ops-rsa.gpg"- strictResult <-- pure- (verifySignedMessage- defaultVerificationOptions- { verificationPolicy = VerifyStrict- , verificationMode = VerificationStreaming- }- kr- signedMessage)- case strictResult of- [Left _] -> assertFailure "verifySignedMessage should succeed for known-good fixture"- verifications ->- assertBool "verifySignedMessage should emit at least one verification" (not (null verifications))--testVerifyV4EdDSASignatureWithEd25519KeyAlgorithm :: Assertion-testVerifyV4EdDSASignatureWithEd25519KeyAlgorithm = do- (signer, signingKey) <- loadDeterministicEd25519Signer- let signerWithEd25519Pka = setPKAlgorithm PKA.Ed25519 signer- signerV4 <- expectV4PKPayload "Ed25519 v4 signer with Ed25519 key algorithm" signerWithEd25519Pka- let payload = "v4 eddsa signature with Ed25519 key algorithm"- state = emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}- keyring = mkTestKeyring [TKUnknown (signerWithEd25519Pka, Nothing) [] [] [] []]- signedResult <-- signMessageWith- (mkEd25519SignerV4 signerV4 signingKey)- (mkClearPayload payload)- signaturePkt <-- case signedResult of- Left err ->- assertFailure ("Ed25519 message signing failed: " ++ show err) >>- fail "expected signed Ed25519 payload"- Right signedMessage ->- case parsePkts signedMessage of- [LiteralDataPkt {}, sig@(SignaturePkt (SigV4 BinarySig PKA.Ed25519 SHA512 _ _ _ _))] ->- pure sig- other ->- assertFailure- ("Expected [LiteralDataPkt, Ed25519 SigV4] for Ed25519-key-algorithm verification test, got " ++- show other) >>- fail "unexpected Ed25519-key-algorithm signature shape"- case verifySigWith (verifyAgainstKeyring keyring) signaturePkt state Nothing of- Left err ->- assertFailure- ("Ed25519 SigV4 verification with Ed25519 key algorithm failed: " ++- renderVerificationError err)- Right _ -> pure ()+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Tests.MessageAndArmor (messageAndArmorTests) where++import Codec.Encryption.OpenPGP.ASCIIArmor.Types+ ( Armor (..)+ , ArmorType (..)+ )+import qualified Crypto.PubKey.Ed25519 as Ed25519+import qualified Crypto.PubKey.RSA as RSA+import Data.Binary (get, put)+import Data.Binary.Get (Get, runGetOrFail)+import Data.Binary.Put+ ( putByteString+ , putWord16be+ , putWord32be+ , putWord8+ , runPut+ )+import Data.Bits (xor)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import Data.Either (isLeft, isRight)+import Data.List (isInfixOf)+import qualified Data.List.NonEmpty as NE+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit+ ( Assertion+ , assertBool+ , assertEqual+ , assertFailure+ , testCase+ )++import Codec.Encryption.OpenPGP.BlockCipher (keySize)+import Codec.Encryption.OpenPGP.CFB+ ( decryptPreservingNonce+ , validateSEIPD1MDC+ )+import Codec.Encryption.OpenPGP.Encrypt+ ( encryptSEIPDv2WithSKESKBlock+ )+import Codec.Encryption.OpenPGP.Fingerprint+ ( eightOctetKeyID+ , fingerprint+ )+import Codec.Encryption.OpenPGP.Internal (emptyPSC, lastLD)+import Codec.Encryption.OpenPGP.KeyringParser (parseUnknownTKs)+import Codec.Encryption.OpenPGP.Message+import Codec.Encryption.OpenPGP.Policy+ ( defaultVerificationPolicy+ , signatureV6SaltSizeForHashAlgorithm+ )+import Codec.Encryption.OpenPGP.S2K (renderS2KError, string2Key)+import Codec.Encryption.OpenPGP.Serialize+ ( armorPayloadsOfType+ , parsePkts+ , parsePktsEither+ , recommendedArmorType+ , singleArmorPayloadOfType+ , singleClearSignedBlock+ )+import Codec.Encryption.OpenPGP.SerializeForSigs+ ( payloadForSig+ , payloadForSigWith+ )+import Codec.Encryption.OpenPGP.Signatures+ ( SignError (..)+ , VerificationError (..)+ , renderSignError+ , renderVerificationError+ , signCertRevocationWithRSA+ , signCertificationWithRSA+ , signDataWithEd25519+ , signDataWithEd25519Builder+ , signDataWithEd25519V6+ , signDataWithEd25519V6Builder+ , signDataWithEd448+ , signDataWithEd448V6+ , signDataWithRSA+ , signDataWithRSABuilder+ , signDirectKeyWithRSA+ , signKeyRevocationWithRSA+ , signSubkeyRevocationWithRSA+ , verifyAgainstKeyring+ , verifyAgainstKeys+ , verifySigWith+ )+import Codec.Encryption.OpenPGP.Subpackets+ ( TextNormalizationMode (..)+ , addHashedSubs+ , addUnhashedSubs+ , listToHashedSubs+ , listToUnhashedSubs+ , sbTextNormMode+ , sigBuilderInit+ )+import Codec.Encryption.OpenPGP.Types+import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as PKA+import Data.Conduit.OpenPGP.Message+ ( verifyMessage+ , verifyMessagePackets+ )+import Data.Conduit.OpenPGP.Verify (VerificationMode (..))+import Tests.Common+ ( addTimestampSeconds+ , assertSingleFailureContainsTimeline+ , assertSingleSignerFingerprint+ , encryptMessageDefault+ , expectV4PKPayload+ , expectV6PKPayload+ , extractV4SignatureAlgorithmFields+ , fp+ , loadAndDecompressPkts+ , loadArmor+ , loadDeterministicEd25519Signer+ , loadDeterministicEd25519SignerV6+ , loadDeterministicEd448Signer+ , loadDeterministicEd448SignerV6+ , loadKeyring+ , loadUnencryptedRsaSigner+ , loadUnencryptedRsaSignerV6+ , messageIssuerSubpacketsAt+ , mkTestKeyring+ , readFixtureLazy+ , setPKAlgorithm+ , signBinaryMessageWithEd25519At+ , signBinaryMessageWithRSAAt+ , signKeyRevocationWithReasonAndExtrasAt+ , signKeyRevocationWithReasonAt+ , signSubkeyBindingWithRSAAt+ , signSubkeyRevocationWithRSAAt+ , verifyTimelinePackets+ )++messageAndArmorTests :: TestTree+messageAndArmorTests =+ testGroup+ "Message API and armor fixtures"+ [ testGroup+ "Message API group"+ [ testCase+ "default encrypt/decrypt message"+ testDefaultedEncryptDecrypt+ , testCase+ "specific encrypt/decrypt message"+ testExplicitEncryptDecrypt+ , testCase+ "specific encrypt can expose effective session material"+ testExplicitEncryptExposesEffectiveSessionMaterial+ , testCase+ "default encrypt does not expose session material"+ testDefaultEncryptDoesNotExposeSessionMaterial+ , testCase+ "AES-128 EAX encrypt/decrypt message"+ testExplicitAES128EAXEncryptDecrypt+ , testCase+ "AES-128 GCM encrypt/decrypt message"+ testExplicitAES128GCMEncryptDecrypt+ , testCase+ "specific encrypt rejects deprecated S2K hash in modern mode"+ testExplicitEncryptRejectsDeprecatedS2KHash+ , testCase+ "legacy fallback encrypt/decrypt message"+ testLegacyFallbackEncryptDecrypt+ , testCase+ "RFC4880 encryptMessage SEIPDv1 cleartext parses without trailing junk"+ testRFC4880EncryptMessageSEIPDv1ParsesCleanly+ , testCase+ "decryptMessage returns typed parse failures"+ testDecryptMessageTypedParseFailure+ , testCase+ "decryptMessage rejects unknown critical packets"+ testDecryptMessageRejectsUnknownCriticalPacket+ , testCase+ "decryptMessage returns typed decrypt failures"+ testDecryptMessageTypedDecryptFailure+ , testCase+ "decryptMessage rejects SEIPDv1 MDC tampering"+ testDecryptMessageSEIPDv1MDCTampering+ , testCase "sign message shape" testSignMessageShape+ , testCase "sign message shape (RSA SigV6)" testSignMessageRSAV6+ , testCase "sign message shape (Ed25519)" testSignMessageEd25519+ , testCase+ "sign message shape (Ed25519 SigV6)"+ testSignMessageEd25519V6+ , testCase "sign message shape (Ed448)" testSignMessageEd448+ , testCase+ "sign message shape (Ed448 SigV6)"+ testSignMessageEd448V6+ , testCase+ "v4 Ed25519/Ed448 signatures use native fixed-width encoding"+ testV4EdSignaturesUseNativeFixedWidthEncoding+ , testCase+ "v4 Ed25519Legacy key parsing rejects missing 0x40 prefix"+ testV4Ed25519LegacyKeyRejectsMissingPrefix+ , testCase+ "detached v4 Ed25519 verification tolerates missing issuer hints"+ testVerifyDetachedEd25519WithoutIssuerHints+ , testCase+ "detached v4 Ed25519 verification tolerates fake issuer hints"+ testVerifyDetachedEd25519WithFakeIssuerHint+ , testCase+ "detached v4 Ed25519 verifyAgainstKeys tolerates missing issuer hints"+ testVerifyDetachedEd25519WithoutIssuerHintsAgainstKeys+ , testCase+ "detached v4 Ed25519 verifyAgainstKeys tolerates fake issuer hints"+ testVerifyDetachedEd25519WithFakeIssuerHintAgainstKeys+ , testCase+ "v4 EdDSA signatures verify with Ed25519 key algorithm identifier"+ testVerifyV4EdDSASignatureWithEd25519KeyAlgorithm+ , testCase+ "sign message convenience API"+ testSignMessageConvenience+ , testCase+ "typed verify surface matches legacy results"+ testTypedVerifySurfaceMatchesLegacy+ , testCase+ "strict typed verify rejects tamper"+ testVerifyMessageStrictRejectsTamper+ , testCase+ "verifySignedMessage convenience API"+ testVerifySignedMessageConvenience+ , testCase+ "signature generation primitives"+ testSignaturePrimitives+ , testCase+ "typed signature payload coercions"+ testSignatureDataKindsCoercions+ , testCase+ "typed armor payload helpers centralize block selection"+ testTypedArmorPayloadHelpers+ , testCase+ "recommendedArmorType infers canonical armor labels from packets"+ testRecommendedArmorType+ , testCase+ "singleClearSignedBlock validates cleartext signature envelopes"+ testSingleClearSignedBlock+ , testCase+ "canonical text signature payload normalization"+ testCanonicalTextSigPayloadNormalization+ , testCase+ "canonical text signatures normalize during signing"+ testCanonicalTextSignatureSigningPaths+ , testCase+ "text normalization modes (RFC9580Strict vs CleartextCompat)"+ testTextNormalizationModes+ ]+ , testGroup+ "ASCII armor fixture group"+ [ testCase+ "v4-encrypted-secret.pgp.aa decodes as an encrypted v4 secret key"+ testV4EncryptedSecretArmor+ , testCase+ "v4-encrypted.rev.aa decodes as a v4 revocation certificate"+ testV4EncryptedRevocationArmor+ , testCase+ "v4-encrypted.rev.aa SigV4 key-revocation semantics"+ testV4RevocationSignatureSemantics+ , testCase+ "v4-encrypted.rev.aa parses as single revocation TKUnknown"+ ( testRevocationArmorParsesAsSingleTransferableKey+ "v4-encrypted.rev.aa"+ True+ )+ , testCase+ "v6.rev.aa decodes as a v6 revocation certificate"+ (testV6RevocationArmor "v6.rev.aa")+ , testCase+ "v6.rev.aa SigV6 salt semantics"+ (testV6RevocationSignatureSaltSemantics "v6.rev.aa")+ , testCase+ "v6.rev.aa forbids legacy Issuer key-id subpackets"+ (testV6RevocationSignatureRejectsLegacyIssuerKeyID "v6.rev.aa")+ , testCase+ "v6.rev.aa parses as single revocation TKUnknown"+ (testRevocationArmorParsesAsSingleTransferableKey "v6.rev.aa" True)+ , testCase+ "v6-encrypted.rev.aa decodes as a v6 revocation certificate"+ (testV6RevocationArmor "v6-encrypted.rev.aa")+ , testCase+ "v6-encrypted.rev.aa SigV6 salt semantics"+ (testV6RevocationSignatureSaltSemantics "v6-encrypted.rev.aa")+ , testCase+ "v6-encrypted.rev.aa forbids legacy Issuer key-id subpackets"+ ( testV6RevocationSignatureRejectsLegacyIssuerKeyID+ "v6-encrypted.rev.aa"+ )+ , testCase+ "v6-encrypted.rev.aa parses as single revocation TKUnknown"+ ( testRevocationArmorParsesAsSingleTransferableKey+ "v6-encrypted.rev.aa"+ True+ )+ , testCase+ "msg1.asc decodes as a parseable armored message"+ testMsg1ArmorFixture+ ]+ , testGroup+ "timeline-aware group"+ [ testCase+ "timeline-aware primary soft revocation keeps pre-revocation signatures"+ testSignerTimelineSoftPrimaryRevocation+ , testCase+ "timeline-aware primary hard revocation rejects historical signatures"+ testSignerTimelineHardPrimaryRevocation+ , testCase+ "timeline-aware no-reason primary revocation rejects historical signatures"+ testSignerTimelineNoReasonPrimaryRevocation+ , testCase+ "timeline-aware unknown-reason primary revocation rejects historical signatures"+ testSignerTimelineUnknownReasonPrimaryRevocation+ , testCase+ "timeline-aware non-compromise primary revocation stays historical until effective"+ testSignerTimelineNonCompromisePrimaryRevocationIsHistorical+ , testCase+ "timeline-aware temporary primary revocation expires and restores validity"+ testSignerTimelineTemporaryPrimaryRevocationExpires+ , testCase+ "timeline-aware subkey revocation distinguishes pre/post signatures"+ testSignerTimelineSubkeyRevocation+ ]+ ]++testV4EncryptedSecretArmor :: Assertion+testV4EncryptedSecretArmor = do+ armors <- loadArmor "v4-encrypted-secret.pgp.aa"+ armor <-+ case armors of+ [a] -> pure a+ _ ->+ assertFailure+ "v4 encrypted secret fixture should contain one armored payload"+ >> fail "expected one armored payload"+ (headers, payload) <-+ case armor of+ Armor ArmorPrivateKeyBlock hs p -> pure (hs, p)+ Armor atype _ _ ->+ assertFailure+ ( "v4 encrypted secret fixture should decode as a private key block, got "+ ++ show atype+ )+ >> fail "expected private key block"+ _ ->+ assertFailure+ "v4 encrypted secret fixture should decode as an armored payload"+ >> fail "expected armored payload"+ assertEqual+ "v4 encrypted secret fixture should keep identifying comments"+ [ ("Comment", "D2E0 81E9 3FDC A2E7 8B5F C433 811D 9243 394B 79C1")+ , ("Comment", "<v4test@example.org>")+ , ("Comment", "v4 Test User")+ ]+ headers+ let packets = parsePkts payload+ userIds = [u | UserIdPkt u <- packets]+ primarySecretKeys = [(pkp, ska) | SecretKeyPkt pkp ska <- packets]+ secretSubkeys = [(pkp, ska) | SecretSubkeyPkt pkp ska <- packets]+ assertEqual+ "v4 encrypted secret fixture should include expected user IDs"+ ["<v4test@example.org>", "v4 Test User"]+ userIds+ assertEqual+ "v4 encrypted secret fixture should contain one primary secret key packet"+ 1+ (length primarySecretKeys)+ assertEqual+ "v4 encrypted secret fixture should contain three encrypted secret subkeys"+ 3+ (length secretSubkeys)+ case packets of+ (SecretKeyPkt pkp ska : _) -> do+ assertEqual+ "v4 encrypted secret fixture should contain a v4 primary key"+ V4+ (_keyVersion pkp)+ assertEqual+ "v4 encrypted secret fixture should have the expected primary-key fingerprint"+ (fp "D2E0 81E9 3FDC A2E7 8B5F C433 811D 9243 394B 79C1")+ (fingerprint pkp)+ assertEqual+ "v4 encrypted secret fixture should use EdDSA for its primary key"+ EdDSA+ (_pkalgo pkp)+ assertEncryptedS2K "primary key" ska+ mapM_ (assertEncryptedS2K "subkey" . snd) secretSubkeys+ assertEqual+ "v4 encrypted secret fixture should contain two EdDSA subkeys and one ECDH subkey"+ [EdDSA, EdDSA, ECDH]+ (map (_pkalgo . fst) secretSubkeys)+ _ ->+ assertFailure+ "v4 encrypted secret fixture should start with a secret key packet"+ where+ assertEncryptedS2K :: String -> SKAddendum -> Assertion+ assertEncryptedS2K testLabel ska =+ case ska of+ SUSSHA1 AES256 (IteratedSalted SHA256 _ iter) _ encryptedPayload -> do+ assertEqual+ (testLabel ++ " should use expected S2K iteration count")+ (IterationCount 65011712)+ iter+ if BL.null encryptedPayload+ then+ assertFailure+ (testLabel ++ " should have non-empty encrypted key material")+ else pure ()+ SUUnencrypted _ _ ->+ assertFailure+ ( testLabel+ ++ " should be encrypted, got unencrypted secret material"+ )+ _ ->+ assertFailure+ ( testLabel+ ++ " should be encrypted with SUSSHA1/AES256/IteratedSalted SHA256"+ )++testV4EncryptedRevocationArmor :: Assertion+testV4EncryptedRevocationArmor = do+ armors <- loadArmor "v4-encrypted.rev.aa"+ armor <-+ case armors of+ [a] -> pure a+ _ ->+ assertFailure+ "v4 encrypted revocation fixture should contain one armored payload"+ >> fail "expected one armored payload"+ (headers, payload) <-+ case armor of+ Armor ArmorPublicKeyBlock hs p -> pure (hs, p)+ Armor atype _ _ ->+ assertFailure+ ( "v4 encrypted revocation fixture should decode as a public key block, got "+ ++ show atype+ )+ >> fail "expected public key block"+ _ ->+ assertFailure+ "v4 encrypted revocation fixture should decode as an armored payload"+ >> fail "expected armored payload"+ assertEqual+ "v4 encrypted revocation fixture should keep identifying comments"+ [ ("Comment", "Revocation certificate for")+ , ("Comment", "D2E0 81E9 3FDC A2E7 8B5F C433 811D 9243 394B 79C1")+ , ("Comment", "<v4test@example.org>")+ , ("Comment", "v4 Test User")+ ]+ headers+ let packets = parsePkts payload+ case packets of+ [PublicKeyPkt pkp, SignaturePkt _] -> do+ assertEqual+ "v4 encrypted revocation fixture should contain a v4 public key"+ V4+ (_keyVersion pkp)+ assertEqual+ "v4 encrypted revocation fixture should contain the expected public key fingerprint"+ (fp "D2E0 81E9 3FDC A2E7 8B5F C433 811D 9243 394B 79C1")+ (fingerprint pkp)+ assertEqual+ "v4 encrypted revocation fixture should use EdDSA for its public key"+ EdDSA+ (_pkalgo pkp)+ _ ->+ assertFailure+ ( "v4 encrypted revocation fixture should contain [PublicKeyPkt, SignaturePkt], got: "+ ++ show packets+ )++testV6RevocationArmor :: FilePath -> Assertion+testV6RevocationArmor fixture = do+ armors <- loadArmor fixture+ armor <-+ case armors of+ [a] -> pure a+ _ ->+ assertFailure+ (fixture ++ " fixture should contain one armored payload")+ >> fail "expected one armored payload"+ payload <-+ case armor of+ Armor ArmorPublicKeyBlock _ p -> pure p+ Armor atype _ _ ->+ assertFailure+ ( fixture+ ++ " fixture should decode as a public key block, got "+ ++ show atype+ )+ >> fail "expected public key block"+ _ ->+ assertFailure+ (fixture ++ " fixture should decode as an armored payload")+ >> fail "expected armored payload"+ let packets = parsePkts payload+ case packets of+ [PublicKeyPkt pkp, SignaturePkt sig] -> do+ assertEqual+ (fixture ++ " should contain a v6 public key")+ V6+ (_keyVersion pkp)+ case sig of+ SigV6 KeyRevocationSig _ _ _ hashed unhashed _ _ -> do+ let issuerFps =+ [ ifp+ | SigSubPacket _ (IssuerFingerprint IssuerFingerprintV6 ifp) <-+ hashed ++ unhashed+ ]+ if fingerprint pkp `elem` issuerFps+ then pure ()+ else+ assertFailure+ ( fixture+ ++ " should include an IssuerFingerprint v6 matching the public key"+ )+ _ ->+ assertFailure+ (fixture ++ " should contain a SigV6 key-revocation signature")+ _ ->+ assertFailure+ ( fixture+ ++ " should contain [PublicKeyPkt, SignaturePkt], got: "+ ++ show packets+ )++testV6RevocationSignatureSaltSemantics :: FilePath -> Assertion+testV6RevocationSignatureSaltSemantics fixture = do+ armors <- loadArmor fixture+ payload <-+ case armors of+ [Armor ArmorPublicKeyBlock _ p] -> pure p+ _ ->+ assertFailure+ (fixture ++ " should contain one armored public-key payload")+ >> fail "expected one armored payload"+ let packets = parsePkts payload+ (pkp, sig) <-+ case packets of+ [PublicKeyPkt pk, SignaturePkt sigV6@(SigV6 _ _ _ _ _ _ _ _)] -> pure (pk, sigV6)+ _ ->+ assertFailure+ ( fixture+ ++ " should parse as [PublicKeyPkt, SignaturePkt SigV6], got "+ ++ show packets+ )+ >> fail "unexpected revocation fixture packet shape"+ case sig of+ SigV6 _ _ ha salt _ _ _ _ ->+ case expectedV6SaltSizeForTest ha of+ Nothing ->+ assertFailure+ ( fixture+ ++ " uses unsupported v6 signature salt hash algorithm: "+ ++ show ha+ )+ Just expected ->+ assertEqual+ (fixture ++ " SigV6 salt size should match hash algorithm")+ expected+ (fromIntegral (BL.length (unSignatureSalt salt)))+ other ->+ assertFailure+ ( fixture+ ++ " expected SigV6 revocation signature payload, got: "+ ++ show other+ )+ assertBool+ ( fixture+ ++ " should include IssuerFingerprint v6 for the revoked key"+ )+ (signatureHasIssuerFingerprintV6 (fingerprint pkp) sig)++testV4RevocationSignatureSemantics :: Assertion+testV4RevocationSignatureSemantics = do+ armors <- loadArmor "v4-encrypted.rev.aa"+ payload <-+ case armors of+ [Armor ArmorPublicKeyBlock _ p] -> pure p+ _ ->+ assertFailure+ "v4-encrypted.rev.aa should contain one armored public-key payload"+ >> fail "expected one armored payload"+ let packets = parsePkts payload+ case packets of+ [ PublicKeyPkt _+ , SignaturePkt (SigV4 KeyRevocationSig _ _ hashed unhashed _ _)+ ] -> do+ assertBool+ "v4-encrypted.rev.aa key-revocation signature should include an Issuer key-id subpacket"+ (any isIssuerSubpacket (hashed ++ unhashed))+ _ ->+ assertFailure+ ( "v4-encrypted.rev.aa should parse as [PublicKeyPkt, SignaturePkt SigV4 KeyRevocationSig], got "+ ++ show packets+ )+ where+ isIssuerSubpacket (SigSubPacket _ Issuer {}) = True+ isIssuerSubpacket _ = False++testV6RevocationSignatureRejectsLegacyIssuerKeyID+ :: FilePath -> Assertion+testV6RevocationSignatureRejectsLegacyIssuerKeyID fixture = do+ armors <- loadArmor fixture+ payload <-+ case armors of+ [Armor ArmorPublicKeyBlock _ p] -> pure p+ _ ->+ assertFailure+ (fixture ++ " should contain one armored public-key payload")+ >> fail "expected one armored payload"+ let packets = parsePkts payload+ case packets of+ [ PublicKeyPkt _+ , SignaturePkt (SigV6 KeyRevocationSig _ _ _ hashed unhashed _ _)+ ] ->+ assertBool+ ( fixture+ ++ " SigV6 key-revocation signature must not include Issuer key-id subpackets"+ )+ (all (not . isIssuerSubpacket) (hashed ++ unhashed))+ _ ->+ assertFailure+ ( fixture+ ++ " should parse as [PublicKeyPkt, SignaturePkt SigV6 KeyRevocationSig]"+ )+ where+ isIssuerSubpacket (SigSubPacket _ Issuer {}) = True+ isIssuerSubpacket _ = False++testRevocationArmorParsesAsSingleTransferableKey+ :: FilePath -> Bool -> Assertion+testRevocationArmorParsesAsSingleTransferableKey fixture expectDirectRevs = do+ armors <- loadArmor fixture+ payload <-+ case armors of+ [Armor ArmorPublicKeyBlock _ p] -> pure p+ _ ->+ assertFailure+ (fixture ++ " should contain one armored public-key payload")+ >> fail "expected one armored payload"+ let tks = parseUnknownTKs True (parsePkts payload)+ case tks of+ [tk] -> do+ if expectDirectRevs+ then+ assertBool+ ( fixture+ ++ " transferable key should contain at least one direct-key revocation signature"+ )+ (not (null (_tkuRevs tk)))+ else pure ()+ assertEqual+ (fixture ++ " revocation certificate should not carry user IDs")+ []+ (_tkuUIDs tk)+ assertEqual+ ( fixture+ ++ " revocation certificate should not carry user attributes"+ )+ []+ (_tkuUAts tk)+ assertEqual+ (fixture ++ " revocation certificate should not carry subkeys")+ []+ (_tkuSubs tk)+ _ ->+ assertFailure+ (fixture ++ " should parse into exactly one transferable key")++testMsg1ArmorFixture :: Assertion+testMsg1ArmorFixture = do+ armors <- loadArmor "msg1.asc"+ assertBool+ "msg1.asc should decode to at least one armor block"+ (not (null armors))+ let packetBlocks =+ [ parsePkts payload+ | Armor _ _ payload <- armors+ ]+ assertBool+ "msg1.asc should contain at least one parseable packet block"+ (any (not . null) packetBlocks)++expectedV6SaltSizeForTest :: HashAlgorithm -> Maybe Int+expectedV6SaltSizeForTest =+ fmap fromIntegral . signatureV6SaltSizeForHashAlgorithm++signatureHasIssuerFingerprintV6+ :: Fingerprint -> SignaturePayload -> Bool+signatureHasIssuerFingerprintV6 expectedFp (SigV6 _ _ _ _ hashed unhashed _ _) =+ expectedFp+ `elem` [ ifp+ | SigSubPacket _ (IssuerFingerprint IssuerFingerprintV6 ifp) <-+ hashed ++ unhashed+ ]+signatureHasIssuerFingerprintV6 _ _ = False++testDefaultedEncryptDecrypt :: Assertion+testDefaultedEncryptDecrypt = do+ let passphrase = mkPassphrase "roundtrip1"+ payload = mkClearPayload "hello from a balloon farm on Mars"+ encryptedResult =+ encryptMessageDefault+ DoNotExposeSessionMaterial+ passphrase+ payload+ encrypted <-+ case encryptedResult of+ Left err ->+ assertFailure ("encryptMessageDefault failed: " ++ show err)+ >> fail "encryptMessageDefault failed"+ Right (bs, _) -> pure bs+ case parsePkts (encryptedPayloadBytes encrypted) of+ [ SKESKPkt+ (SKESKPayloadV6Packet (SKESKPayloadV6 AES256 OCB _ iv esk tag))+ , SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 6 _ _)+ ] -> do+ assertBool+ "default SKESK v6 IV should be present"+ (not (BL.null iv))+ assertBool+ "default SKESK v6 wrapped session key should be present"+ (not (BL.null esk))+ assertEqual "default SKESK v6 tag length" 16 (BL.length tag)+ other ->+ assertFailure+ ( "default encryption should emit SKESK v6 + SEIPD v2 packets, got "+ ++ show other+ )+ decrypted <-+ case decryptMessage passphrase encrypted of+ Left err ->+ assertFailure ("decryptMessage failed: " ++ show err)+ >> fail "decryptMessage failed"+ Right bs -> pure bs+ assertEqual+ "default encrypt/decrypt payload roundtrip"+ payload+ decrypted++testExplicitEncryptDecrypt :: Assertion+testExplicitEncryptDecrypt = do+ let passphrase = mkPassphrase "roundtrip2"+ payload = mkClearPayload "hello from nonsenseville"+ s2k = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15+ iv = IV "1234567890ABCDEF"+ encrypted <-+ case encryptMessage+ ( RFC9580EncryptMessageOptions+ { rfc9580EncryptMessageExposure = DoNotExposeSessionMaterial+ , rfc9580EncryptMessageSymmetricAlgorithm = AES128+ , rfc9580EncryptMessageS2K = s2k+ , rfc9580EncryptMessageIV = iv+ }+ )+ passphrase+ payload of+ Left err ->+ assertFailure ("micro-managed encryption failed: " ++ show err)+ >> fail "encryptMessage failed"+ Right (bs, _) -> pure bs+ case parsePkts (encryptedPayloadBytes encrypted) of+ [ SKESKPkt+ ( SKESKPayloadV6Packet+ (SKESKPayloadV6 AES128 OCB _ packetIv esk tag)+ )+ , SymEncIntegrityProtectedDataPkt (SEIPD2 AES128 OCB 6 _ _)+ ] -> do+ assertBool+ "explicit SKESK v6 IV should be present"+ (not (BL.null packetIv))+ assertBool+ "explicit SKESK v6 wrapped session key should be present"+ (not (BL.null esk))+ assertEqual "explicit SKESK v6 tag length" 16 (BL.length tag)+ other ->+ assertFailure+ ( "explicit AES encryption should default to SKESK v6 + SEIPD v2 packets, got "+ ++ show other+ )+ decrypted <-+ case decryptMessage passphrase encrypted of+ Left err ->+ assertFailure ("decryption failed: " ++ show err)+ >> fail "decryptMessage failed"+ Right bs -> pure bs+ assertEqual+ "specific encrypt/decrypt payload roundtrip"+ payload+ decrypted++testExplicitEncryptExposesEffectiveSessionMaterial :: Assertion+testExplicitEncryptExposesEffectiveSessionMaterial = do+ let passphraseBytes = "roundtrip2-session-material"+ passphrase = mkPassphrase passphraseBytes+ payload = mkClearPayload "hello from session material town"+ sa = AES128+ s2k = Argon2 (Salt16 (B.pack [0x40 .. 0x4f])) 1 4 15+ iv = IV "ABCDEF1234567890"+ (encrypted, recovered) <-+ case encryptMessage+ ( RFC9580EncryptMessageOptions+ { rfc9580EncryptMessageExposure = ExposeSessionMaterial+ , rfc9580EncryptMessageSymmetricAlgorithm = sa+ , rfc9580EncryptMessageS2K = s2k+ , rfc9580EncryptMessageIV = iv+ }+ )+ passphrase+ payload of+ Left err ->+ assertFailure+ ("explicit encryption with session material failed: " ++ show err)+ >> fail "encryptMessage failed"+ Right x -> pure x+ expectedSessionKey <-+ case keySize sa of+ Left err ->+ assertFailure+ ("failed to derive expected key size: " ++ show err)+ >> fail "keySize failed"+ Right keyLen ->+ case string2Key s2k keyLen passphraseBytes of+ Left err ->+ assertFailure+ ("failed to derive expected session key: " ++ renderS2KError err)+ >> fail "string2Key failed"+ Right bs -> pure bs+ case recovered of+ Just recoveredSessionMaterial -> do+ assertEqual+ "recovered session material algorithm"+ sa+ (recoveredSessionAlgorithm recoveredSessionMaterial)+ assertEqual+ "recovered session material key"+ (SessionKey expectedSessionKey)+ (recoveredSessionKey recoveredSessionMaterial)+ Nothing ->+ assertFailure "expected exposed session material"+ decrypted <-+ case decryptMessage passphrase encrypted of+ Left err ->+ assertFailure ("decryption failed: " ++ show err)+ >> fail "decryptMessage failed"+ Right bs -> pure bs+ assertEqual+ "session-material exposing encrypt/decrypt payload roundtrip"+ payload+ decrypted++testDefaultEncryptDoesNotExposeSessionMaterial :: Assertion+testDefaultEncryptDoesNotExposeSessionMaterial = do+ let passphrase = mkPassphrase "roundtrip-default-no-session-material"+ payload = mkClearPayload "hello from hidden-session-material town"+ (_, recovered) <-+ do+ let encryptedResult =+ encryptMessageDefault+ DoNotExposeSessionMaterial+ passphrase+ payload+ case encryptedResult of+ Left err ->+ assertFailure+ ("default encryption with exposure mode failed: " ++ show err)+ >> fail "encryptMessageDefault failed"+ Right x -> pure x+ case recovered of+ Nothing -> pure ()+ Just _ ->+ assertFailure+ "default exposure mode should not return session material"++testExplicitAES128AEADEncryptDecrypt+ :: AEADAlgorithm -> Int -> String -> Assertion+testExplicitAES128AEADEncryptDecrypt aead expectedIvLen label = do+ let passphraseBytes =+ "roundtrip-"+ <> BL.fromStrict (B.pack (map (fromIntegral . fromEnum) label))+ passphrase = mkPassphrase passphraseBytes+ payload =+ mkClearPayload+ ( "hello from "+ <> BL.fromStrict (B.pack (map (fromIntegral . fromEnum) label))+ )+ s2k = Argon2 (Salt16 (B.pack [0x10 .. 0x1f])) 1 4 15+ salt = Salt (B.pack [0x20 .. 0x3f])+ block =+ Block+ [ LiteralDataPkt BinaryData BL.empty 0 (clearPayloadBytes payload)+ ]+ packets <-+ case encryptSEIPDv2WithSKESKBlock+ AES128+ aead+ 6+ salt+ s2k+ passphraseBytes+ block of+ Left err ->+ assertFailure+ ("AES-128 " ++ label ++ " encryption failed: " ++ err)+ >> fail "encryptSEIPDv2WithSKESKBlock failed"+ Right ps -> pure ps+ let encrypted = mkEncryptedPayload (runPut (put (Block packets)))+ case parsePkts (encryptedPayloadBytes encrypted) of+ [ SKESKPkt+ ( SKESKPayloadV6Packet+ (SKESKPayloadV6 AES128 parsedAead _ iv esk tag)+ )+ , SymEncIntegrityProtectedDataPkt (SEIPD2 AES128 payloadAead 6 _ _)+ ] -> do+ assertEqual+ ("AES-128 " ++ label ++ " SKESK AEAD")+ aead+ parsedAead+ assertEqual+ ("AES-128 " ++ label ++ " payload AEAD")+ aead+ payloadAead+ assertEqual+ ("AES-128 " ++ label ++ " SKESK v6 IV length")+ (fromIntegral expectedIvLen)+ (BL.length iv)+ assertBool+ ("AES-128 " ++ label ++ " wrapped session key should be present")+ (not (BL.null esk))+ assertEqual+ ("AES-128 " ++ label ++ " SKESK tag length")+ 16+ (BL.length tag)+ other ->+ assertFailure+ ( "AES-128 "+ ++ label+ ++ " encryption should emit matching SKESK v6 + SEIPD v2 packets, got "+ ++ show other+ )+ decrypted <-+ case decryptMessage passphrase encrypted of+ Left err ->+ assertFailure+ ("AES-128 " ++ label ++ " decryption failed: " ++ show err)+ >> fail "decryptMessage failed"+ Right bs -> pure bs+ assertEqual+ ("AES-128 " ++ label ++ " payload roundtrip")+ payload+ decrypted++testExplicitAES128EAXEncryptDecrypt :: Assertion+testExplicitAES128EAXEncryptDecrypt = do+ let passphraseBytes = "roundtrip-EAX"+ s2k = Argon2 (Salt16 (B.pack [0x10 .. 0x1f])) 1 4 15+ salt = Salt (B.pack [0x20 .. 0x3f])+ block = Block [LiteralDataPkt BinaryData BL.empty 0 "hello from EAX"]+ case encryptSEIPDv2WithSKESKBlock+ AES128+ EAX+ 6+ salt+ s2k+ passphraseBytes+ block of+ Left err+ | "EAX is currently unsupported by the crypton AEAD backend"+ `isInfixOf` err ->+ pure ()+ | otherwise ->+ assertFailure+ ("expected explicit EAX backend limitation, got: " ++ err)+ Right packets ->+ assertFailure+ ( "expected AES-128 EAX encryption to fail explicitly, got packets: "+ ++ show packets+ )++testExplicitAES128GCMEncryptDecrypt :: Assertion+testExplicitAES128GCMEncryptDecrypt =+ testExplicitAES128AEADEncryptDecrypt GCM 12 "GCM"++testExplicitEncryptRejectsDeprecatedS2KHash :: Assertion+testExplicitEncryptRejectsDeprecatedS2KHash = do+ let passphrase = mkPassphrase "roundtrip2b"+ payload = mkClearPayload "modern-path deprecated s2k hash rejection"+ s2k = Salted SHA1 (Salt8 "12345678")+ iv = IV "1234567890ABCDEF"+ case encryptMessage+ ( RFC9580EncryptMessageOptions+ { rfc9580EncryptMessageExposure = DoNotExposeSessionMaterial+ , rfc9580EncryptMessageSymmetricAlgorithm = AES128+ , rfc9580EncryptMessageS2K = s2k+ , rfc9580EncryptMessageIV = iv+ }+ )+ passphrase+ payload of+ Left (MessageEncryptError err)+ | "deprecated hash algorithm disallowed for modern message generation"+ `isInfixOf` err ->+ pure ()+ | otherwise ->+ assertFailure+ ("Expected deprecated modern S2K hash rejection, got: " ++ err)+ Left err ->+ assertFailure+ ( "Expected MessageEncryptError for deprecated modern S2K hash, got "+ ++ show err+ )+ Right _ ->+ assertFailure+ "Expected encryptMessage AES128 to reject deprecated SHA1 S2K"++testLegacyFallbackEncryptDecrypt :: Assertion+testLegacyFallbackEncryptDecrypt = do+ let passphrase = mkPassphrase "roundtrip3"+ payload = mkClearPayload "hello from legacy town"+ s2k = Salted SHA1 (Salt8 "12345678")+ iv = IV mempty+ encrypted <-+ case encryptMessage+ ( RFC4880EncryptMessageOptions+ { rfc4880EncryptMessageExposure = DoNotExposeSessionMaterial+ , rfc4880EncryptMessageSymmetricAlgorithm = Plaintext+ , rfc4880EncryptMessageS2K = s2k+ , rfc4880EncryptMessageIV = iv+ }+ )+ passphrase+ payload of+ Left err ->+ assertFailure ("legacy fallback encryption failed: " ++ show err)+ >> fail "encryptMessage fallback failed"+ Right (bs, _) -> pure bs+ case parsePkts (encryptedPayloadBytes encrypted) of+ [ SKESKPkt+ (SKESKPayloadV4Packet (SKESKPayloadV4 Plaintext _ Nothing))+ , SymEncIntegrityProtectedDataPkt (SEIPD1 1 _)+ ] -> pure ()+ other ->+ assertFailure+ ( "non-AES explicit encryption should use RFC4880 SKESKv4 + SEIPDv1 packets, got "+ ++ show other+ )+ decrypted <-+ case decryptMessage passphrase encrypted of+ Left err ->+ assertFailure ("legacy fallback decryption failed: " ++ show err)+ >> fail "decryptMessage fallback failed"+ Right bs -> pure bs+ assertEqual+ "legacy fallback encrypt/decrypt payload roundtrip"+ payload+ decrypted++testRFC4880EncryptMessageSEIPDv1ParsesCleanly :: Assertion+testRFC4880EncryptMessageSEIPDv1ParsesCleanly = do+ let passphraseBytes = "legacy-clean-parse" :: BL.ByteString+ passphrase = mkPassphrase passphraseBytes+ payload = mkClearPayload "hello from clean legacy town"+ s2k =+ IteratedSalted SHA256 (Salt8 "saltxyz!") (IterationCount 65536)+ iv = IV "1234567890ABCDEF"+ encrypted <-+ case encryptMessage+ ( RFC4880EncryptMessageOptions+ { rfc4880EncryptMessageExposure = DoNotExposeSessionMaterial+ , rfc4880EncryptMessageSymmetricAlgorithm = AES128+ , rfc4880EncryptMessageS2K = s2k+ , rfc4880EncryptMessageIV = iv+ }+ )+ passphrase+ payload of+ Left err ->+ assertFailure ("RFC4880 encryption failed: " ++ show err)+ >> fail "encryptMessage failed"+ Right (bs, _) -> pure bs+ packets <-+ case parsePktsEither (encryptedPayloadBytes encrypted) of+ Left err ->+ assertFailure ("encrypted packet parse failed: " ++ show err)+ >> fail "parsePktsEither failed"+ Right ps -> pure ps+ ciphertext <-+ case packets of+ [ SKESKPkt+ (SKESKPayloadV4Packet (SKESKPayloadV4 parsedSA parsedS2K Nothing))+ , SymEncIntegrityProtectedDataPkt (SEIPD1 1 payloadBytes)+ ] -> do+ assertEqual "RFC4880 encrypted SKESK algorithm" AES128 parsedSA+ assertEqual "RFC4880 encrypted S2K" s2k parsedS2K+ pure (BL.toStrict payloadBytes)+ other ->+ assertFailure+ ( "RFC4880 encryption should emit SKESKv4 + SEIPDv1 packets, got "+ ++ show other+ )+ >> fail "unexpected encrypted packet layout"+ keyLen <-+ case keySize AES128 of+ Left err ->+ assertFailure ("keySize failed for AES128: " ++ show err)+ >> fail "keySize failed"+ Right n -> pure n+ sessionKey <-+ case string2Key s2k keyLen passphraseBytes of+ Left err ->+ assertFailure ("string2Key failed: " ++ renderS2KError err)+ >> fail "string2Key failed"+ Right keyBytes -> pure keyBytes+ (nonce, decrypted) <-+ case decryptPreservingNonce AES128 ciphertext sessionKey of+ Left err ->+ assertFailure ("decryptPreservingNonce failed: " ++ show err)+ >> fail "decryptPreservingNonce failed"+ Right out -> pure out+ cleartext <-+ case validateSEIPD1MDC nonce decrypted of+ Left err ->+ assertFailure ("validateSEIPD1MDC failed: " ++ err)+ >> fail "validateSEIPD1MDC failed"+ Right out -> pure out+ case parsePktsEither (BL.fromStrict cleartext) of+ Right [LiteralDataPkt BinaryData filename timestamp clearPayload] -> do+ assertEqual+ "RFC4880 decrypted literal filename"+ BL.empty+ filename+ assertEqual "RFC4880 decrypted literal timestamp" 0 timestamp+ assertEqual+ "RFC4880 decrypted literal payload"+ (clearPayloadBytes payload)+ clearPayload+ Right other ->+ assertFailure+ ( "RFC4880 decrypted cleartext should contain exactly one literal packet, got "+ ++ show other+ )+ Left err ->+ assertFailure+ ( "RFC4880 decrypted cleartext should parse without trailing junk, got "+ ++ show err+ )++testDecryptMessageTypedParseFailure :: Assertion+testDecryptMessageTypedParseFailure = do+ let passphrase = mkPassphrase "unused"+ encrypted = mkEncryptedPayload BL.empty+ case decryptMessage passphrase encrypted of+ Left (MessageParseFailureError MissingEncryptedMessage) -> pure ()+ Left err ->+ assertFailure+ ( "Expected MissingEncryptedMessage parse failure, got "+ ++ show err+ )+ Right clear ->+ assertFailure+ ("Expected parse failure, got payload " ++ show clear)++testDecryptMessageRejectsUnknownCriticalPacket :: Assertion+testDecryptMessageRejectsUnknownCriticalPacket = do+ let passphrase = mkPassphrase "unused"+ encrypted =+ mkEncryptedPayload . runPut . put $+ Block [OtherPacketPkt 39 "unknown-critical"]+ case decryptMessage passphrase encrypted of+ Left (MessageParseFailureError (UnknownCriticalPacketType 39)) -> pure ()+ Left err ->+ assertFailure+ ( "Expected UnknownCriticalPacketType 39 parse failure, got "+ ++ show err+ )+ Right clear ->+ assertFailure+ ( "Expected unknown critical packet rejection, got payload "+ ++ show clear+ )++testDecryptMessageTypedDecryptFailure :: Assertion+testDecryptMessageTypedDecryptFailure = do+ let correctPassphrase = mkPassphrase "correct passphrase"+ wrongPassphrase = mkPassphrase "wrong passphrase"+ payload = mkClearPayload "typed decrypt failure payload"+ encryptedResult =+ encryptMessageDefault+ DoNotExposeSessionMaterial+ correctPassphrase+ payload+ encrypted <-+ case encryptedResult of+ Left err ->+ assertFailure ("encryptMessageDefault failed: " ++ show err)+ >> fail "encryptMessageDefault failed"+ Right (bs, _) -> pure bs+ case decryptMessage wrongPassphrase encrypted of+ Left (MessageDecryptFailureError (PayloadDecryptFailed _)) -> pure ()+ Left+ (MessageDecryptFailureError (SessionMaterialDerivationFailed _)) -> pure ()+ Left err ->+ assertFailure+ ("Expected typed decrypt failure, got " ++ show err)+ Right clear ->+ assertFailure+ ("Expected decrypt failure, got payload " ++ show clear)++testDecryptMessageSEIPDv1MDCTampering :: Assertion+testDecryptMessageSEIPDv1MDCTampering = do+ let passphrase = mkPassphrase "mdc-tamper-test"+ payload = mkClearPayload "payload for MDC tampering test"+ s2k =+ IteratedSalted SHA256 (Salt8 "saltxyz!") (IterationCount 65536)+ iv = IV "1234567890ABCDEF"+ encrypted <-+ case encryptMessage+ ( RFC4880EncryptMessageOptions+ { rfc4880EncryptMessageExposure =+ DoNotExposeSessionMaterial+ , rfc4880EncryptMessageSymmetricAlgorithm = AES128+ , rfc4880EncryptMessageS2K = s2k+ , rfc4880EncryptMessageIV = iv+ }+ )+ passphrase+ payload of+ Left err ->+ assertFailure ("SEIPDv1 encryption failed: " ++ show err)+ >> fail "encryptMessage failed"+ Right (bs, _) -> pure bs+ let raw = encryptedPayloadBytes encrypted+ midpoint = BL.length raw `div` 2+ tampered =+ BL.take midpoint raw+ <> BL.cons+ (BL.head (BL.drop midpoint raw) `xor` 0xFF)+ (BL.drop (midpoint + 1) raw)+ case decryptMessage passphrase (mkEncryptedPayload tampered) of+ Left (MessageDecryptFailureError (PayloadDecryptFailed msg))+ | "MDC" `isInfixOf` msg -> pure ()+ | otherwise ->+ assertFailure+ ("Expected MDC-related PayloadDecryptFailed, got: " ++ msg)+ Left err ->+ assertFailure+ ("Expected PayloadDecryptFailed with MDC error, got: " ++ show err)+ Right _ ->+ assertFailure+ "Expected MDC tampering rejection, but decryption succeeded"++testSignMessageShape :: Assertion+testSignMessageShape = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ signerV4 <- expectV4PKPayload "RSA v4 signer" signer+ signedResult <-+ signMessageWith+ (mkRSASignerV4 signerV4 signingKey)+ (mkClearPayload "message-api signing payload")+ signedMessage <-+ case signedResult of+ Left err ->+ assertFailure ("message signing failed: " ++ show err)+ >> pure mempty+ Right bs -> pure bs+ case parsePkts signedMessage of+ [LiteralDataPkt {}, SignaturePkt _] -> pure ()+ _ ->+ assertFailure+ "signing output should contain literal data and one signature packet"++testSignMessageRSAV6 :: Assertion+testSignMessageRSAV6 = do+ (signer, signingKey) <- loadUnencryptedRsaSignerV6+ signerV6 <- expectV6PKPayload "RSA v6 signer" signer+ let payload = "message-api signing payload with RSA SigV6"+ signedResult <-+ signMessageWith+ (mkRSASignerV6 signerV6 signingKey)+ (mkClearPayload payload)+ signedMessage <-+ case signedResult of+ Left err ->+ assertFailure ("RSA SigV6 message signing failed: " ++ show err)+ >> pure mempty+ Right bs -> pure bs+ signaturePkt <-+ case parsePkts signedMessage of+ [ LiteralDataPkt {}+ , sig@(SignaturePkt (SigV6 BinarySig RSA SHA512 salt _ _ _ _))+ ] -> do+ assertEqual+ "SigV6 RSA salt must be 32 bytes for SHA512"+ 32+ (BL.length (unSignatureSalt salt))+ pure sig+ other ->+ assertFailure+ ( "RSA SigV6 signing output should contain [LiteralDataPkt, RSA SigV6], got "+ ++ show other+ )+ >> fail "unexpected RSA SigV6 signMessage output shape"+ let state =+ emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}+ keyring = [TKUnknown (signer, Nothing) [] [] [] []]+ case verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeys keyring)+ signaturePkt+ state+ Nothing of+ Left err ->+ assertFailure+ ( "RSA SigV6 signed message verification failed: "+ ++ renderVerificationError err+ )+ Right _ -> pure ()++testSignMessageEd25519 :: Assertion+testSignMessageEd25519 = do+ (signer, signingKey) <- loadDeterministicEd25519Signer+ signerV4 <- expectV4PKPayload "Ed25519 v4 signer" signer+ let payload = "message-api signing payload with Ed25519"+ signedResult <-+ signMessageWith+ (mkEd25519SignerV4 signerV4 signingKey)+ (mkClearPayload payload)+ signedMessage <-+ case signedResult of+ Left err ->+ assertFailure ("Ed25519 message signing failed: " ++ show err)+ >> pure mempty+ Right bs -> pure bs+ signaturePkt <-+ case parsePkts signedMessage of+ [ LiteralDataPkt {}+ , sig@(SignaturePkt (SigV4 BinarySig PKA.Ed25519 SHA512 _ _ _ _))+ ] ->+ pure sig+ other ->+ assertFailure+ ( "Ed25519 signing output should contain [LiteralDataPkt, Ed25519 SigV4], got "+ ++ show other+ )+ >> fail "unexpected Ed25519 signMessage output shape"+ let state =+ emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}+ keyring = [TKUnknown (signer, Nothing) [] [] [] []]+ case verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeys keyring)+ signaturePkt+ state+ Nothing of+ Left err ->+ assertFailure+ ( "Ed25519 signed message verification failed: "+ ++ renderVerificationError err+ )+ Right _ -> pure ()++testSignMessageEd25519V6 :: Assertion+testSignMessageEd25519V6 = do+ (signer, signingKey) <- loadDeterministicEd25519SignerV6+ signerV6 <- expectV6PKPayload "Ed25519 v6 signer" signer+ let payload = "message-api signing payload with Ed25519 SigV6"+ signedResult <-+ signMessageWith+ (mkEd25519SignerV6 signerV6 signingKey)+ (mkClearPayload payload)+ signedMessage <-+ case signedResult of+ Left err ->+ assertFailure+ ("Ed25519 SigV6 message signing failed: " ++ show err)+ >> pure mempty+ Right bs -> pure bs+ signaturePkt <-+ case parsePkts signedMessage of+ [ LiteralDataPkt {}+ , sig@(SignaturePkt (SigV6 BinarySig PKA.Ed25519 SHA512 salt _ _ _ _))+ ] -> do+ assertEqual+ "SigV6 Ed25519 salt must be 32 bytes for SHA512"+ 32+ (BL.length (unSignatureSalt salt))+ pure sig+ other ->+ assertFailure+ ( "Ed25519 SigV6 signing output should contain [LiteralDataPkt, Ed25519 SigV6], got "+ ++ show other+ )+ >> fail "unexpected Ed25519 SigV6 signMessage output shape"+ case signaturePkt of+ SignaturePkt (SigV6 BinarySig PKA.Ed25519 SHA512 _ _ _ _ _) -> do+ let state =+ emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}+ keyring = [TKUnknown (signer, Nothing) [] [] [] []]+ case verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeys keyring)+ signaturePkt+ state+ Nothing of+ Left err ->+ assertFailure+ ( "Ed25519 SigV6 signed message verification failed: "+ ++ renderVerificationError err+ )+ Right _ -> pure ()+ _ -> assertFailure "expected an Ed25519 SigV6 signature packet"++testV4Ed25519LegacyKeyRejectsMissingPrefix :: Assertion+testV4Ed25519LegacyKeyRejectsMissingPrefix = do+ let legacyEd25519Oid = B.pack [0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01]+ rawEd25519Public = B.replicate 32 0x01+ encoded =+ runPut $ do+ putWord8 4+ putWord32be 0+ putWord8 (fromFVal EdDSA)+ putWord8 (fromIntegral (B.length legacyEd25519Oid))+ putByteString legacyEd25519Oid+ putWord16be 256+ putByteString rawEd25519Public+ case runGetOrFail (get :: Get SomePKPayload) encoded of+ Left (_, _, err) ->+ assertBool+ ("expected invalid-legacy-point parse failure, got: " ++ err)+ ("invalid Ed25519Legacy public key" `isInfixOf` err)+ Right _ ->+ assertFailure+ "legacy Ed25519 key without 0x40 prefix should be rejected"++testSignMessageEd448 :: Assertion+testSignMessageEd448 = do+ (signer, signingKey) <- loadDeterministicEd448Signer+ signerV4 <- expectV4PKPayload "Ed448 v4 signer" signer+ let payload = "message-api signing payload with Ed448"+ signedResult <-+ signMessageWith+ (mkEd448SignerV4 signerV4 signingKey)+ (mkClearPayload payload)+ signedMessage <-+ case signedResult of+ Left err ->+ assertFailure ("Ed448 message signing failed: " ++ show err)+ >> pure mempty+ Right bs -> pure bs+ signaturePkt <-+ case parsePkts signedMessage of+ [ LiteralDataPkt {}+ , sig@(SignaturePkt (SigV4 BinarySig PKA.Ed448 SHA512 _ _ _ _))+ ] ->+ pure sig+ other ->+ assertFailure+ ( "Ed448 signing output should contain [LiteralDataPkt, Ed448 SigV4], got "+ ++ show other+ )+ >> fail "unexpected Ed448 signMessage output shape"+ let state =+ emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}+ keyring = [TKUnknown (signer, Nothing) [] [] [] []]+ case verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeys keyring)+ signaturePkt+ state+ Nothing of+ Left err ->+ assertFailure+ ( "Ed448 signed message verification failed: "+ ++ renderVerificationError err+ )+ Right _ -> pure ()++testSignMessageEd448V6 :: Assertion+testSignMessageEd448V6 = do+ (signer, signingKey) <- loadDeterministicEd448SignerV6+ signerV6 <- expectV6PKPayload "Ed448 v6 signer" signer+ let payload = "message-api signing payload with Ed448 SigV6"+ signedResult <-+ signMessageWith+ (mkEd448SignerV6 signerV6 signingKey)+ (mkClearPayload payload)+ signedMessage <-+ case signedResult of+ Left err ->+ assertFailure+ ("Ed448 SigV6 message signing failed: " ++ show err)+ >> pure mempty+ Right bs -> pure bs+ signaturePkt <-+ case parsePkts signedMessage of+ [ LiteralDataPkt {}+ , sig@(SignaturePkt (SigV6 BinarySig PKA.Ed448 SHA512 salt _ _ _ _))+ ] -> do+ assertEqual+ "SigV6 Ed448 salt must be 32 bytes for SHA512"+ 32+ (BL.length (unSignatureSalt salt))+ pure sig+ other ->+ assertFailure+ ( "Ed448 SigV6 signing output should contain [LiteralDataPkt, Ed448 SigV6], got "+ ++ show other+ )+ >> fail "unexpected Ed448 SigV6 signMessage output shape"+ case signaturePkt of+ SignaturePkt (SigV6 BinarySig PKA.Ed448 SHA512 _ _ _ _ _) -> pure ()+ _ -> assertFailure "expected an Ed448 SigV6 signature packet"++testV4EdSignaturesUseNativeFixedWidthEncoding :: Assertion+testV4EdSignaturesUseNativeFixedWidthEncoding = do+ (_, ed25519SigningKey) <- loadDeterministicEd25519Signer+ ed25519Sig <-+ case signDataWithEd25519+ BinarySig+ ed25519SigningKey+ []+ []+ "v4 ed25519 encoding" of+ Left err ->+ assertFailure+ ("Ed25519 v4 signing failed: " ++ renderSignError err)+ >> fail "expected Ed25519 signature"+ Right sig -> pure sig+ case extractV4SignatureAlgorithmFields (runPut (put ed25519Sig)) of+ Left err ->+ assertFailure+ ( "failed to decode serialized Ed25519 v4 signature payload: "+ ++ err+ )+ Right (pka, algorithmFields) -> do+ assertEqual+ "Ed25519 v4 signature packet algorithm id"+ PKA.Ed25519+ pka+ assertEqual+ "Ed25519 v4 algorithm field width"+ 64+ (B.length algorithmFields)++ (_, ed448SigningKey) <- loadDeterministicEd448Signer+ ed448Sig <-+ case signDataWithEd448+ BinarySig+ ed448SigningKey+ []+ []+ "v4 ed448 encoding" of+ Left err ->+ assertFailure+ ("Ed448 v4 signing failed: " ++ renderSignError err)+ >> fail "expected Ed448 signature"+ Right sig -> pure sig+ case extractV4SignatureAlgorithmFields (runPut (put ed448Sig)) of+ Left err ->+ assertFailure+ ("failed to decode serialized Ed448 v4 signature payload: " ++ err)+ Right (pka, algorithmFields) -> do+ assertEqual+ "Ed448 v4 signature packet algorithm id"+ PKA.Ed448+ pka+ assertEqual+ "Ed448 v4 algorithm field width"+ 114+ (B.length algorithmFields)++testVerifyDetachedEd25519WithoutIssuerHints :: Assertion+testVerifyDetachedEd25519WithoutIssuerHints = do+ (signer, signingKey) <- loadDeterministicEd25519Signer+ let payload = "detached v4 eddsa payload without issuer hints"+ state =+ emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}+ keyring = mkTestKeyring [TKUnknown (signer, Nothing) [] [] [] []]+ sigPayload <-+ case signDataWithEd25519 BinarySig signingKey [] [] payload of+ Left err ->+ assertFailure+ ("Ed25519 detached signing failed: " ++ renderSignError err)+ >> fail (renderSignError err)+ Right sig -> pure sig+ case verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeyring keyring)+ (SignaturePkt sigPayload)+ state+ Nothing of+ Left err ->+ assertFailure+ ( "Ed25519 detached verification without issuer hints failed: "+ ++ renderVerificationError err+ )+ Right _ -> pure ()++testVerifyDetachedEd25519WithFakeIssuerHint :: Assertion+testVerifyDetachedEd25519WithFakeIssuerHint = do+ (signer, signingKey) <- loadDeterministicEd25519Signer+ let payload = "detached v4 eddsa payload with fake issuer hint"+ state =+ emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}+ keyring = mkTestKeyring [TKUnknown (signer, Nothing) [] [] [] []]+ fakeIssuer = EightOctetKeyId "\x01\x23\x45\x67\x89\xab\xcd\xef"+ unhashed = [SigSubPacket False (Issuer fakeIssuer)]+ sigPayload <-+ case signDataWithEd25519 BinarySig signingKey [] unhashed payload of+ Left err ->+ assertFailure+ ("Ed25519 detached signing failed: " ++ renderSignError err)+ >> fail (renderSignError err)+ Right sig -> pure sig+ case verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeyring keyring)+ (SignaturePkt sigPayload)+ state+ Nothing of+ Left err ->+ assertFailure+ ( "Ed25519 detached verification with fake issuer hint failed: "+ ++ renderVerificationError err+ )+ Right _ -> pure ()++testVerifyDetachedEd25519WithoutIssuerHintsAgainstKeys+ :: Assertion+testVerifyDetachedEd25519WithoutIssuerHintsAgainstKeys = do+ (signer, signingKey) <- loadDeterministicEd25519Signer+ let payload =+ "detached v4 eddsa payload without issuer hints (verifyAgainstKeys)"+ state =+ emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}+ keys = [TKUnknown (signer, Nothing) [] [] [] []]+ sigPayload <-+ case signDataWithEd25519 BinarySig signingKey [] [] payload of+ Left err ->+ assertFailure+ ("Ed25519 detached signing failed: " ++ renderSignError err)+ >> fail (renderSignError err)+ Right sig -> pure sig+ case verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeys keys)+ (SignaturePkt sigPayload)+ state+ Nothing of+ Left err ->+ assertFailure+ ( "Ed25519 detached verification against keys without issuer hints failed: "+ ++ renderVerificationError err+ )+ Right _ -> pure ()++testVerifyDetachedEd25519WithFakeIssuerHintAgainstKeys+ :: Assertion+testVerifyDetachedEd25519WithFakeIssuerHintAgainstKeys = do+ (signer, signingKey) <- loadDeterministicEd25519Signer+ let payload =+ "detached v4 eddsa payload with fake issuer hint (verifyAgainstKeys)"+ state =+ emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}+ keys = [TKUnknown (signer, Nothing) [] [] [] []]+ fakeIssuer = EightOctetKeyId "\x01\x23\x45\x67\x89\xab\xcd\xef"+ unhashed = [SigSubPacket False (Issuer fakeIssuer)]+ sigPayload <-+ case signDataWithEd25519 BinarySig signingKey [] unhashed payload of+ Left err ->+ assertFailure+ ("Ed25519 detached signing failed: " ++ renderSignError err)+ >> fail (renderSignError err)+ Right sig -> pure sig+ case verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeys keys)+ (SignaturePkt sigPayload)+ state+ Nothing of+ Left err ->+ assertFailure+ ( "Ed25519 detached verification against keys with fake issuer hint failed: "+ ++ renderVerificationError err+ )+ Right _ -> pure ()++testCanonicalTextSigPayloadNormalization :: Assertion+testCanonicalTextSigPayloadNormalization = do+ let state =+ emptyPSC+ { lastLD =+ LiteralDataPkt+ TextData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ "line1 \t\nline2\t \rline3\t \r\nline4 \t"+ }+ assertEqual+ "Binary signatures preserve original line endings"+ "line1 \t\nline2\t \rline3\t \r\nline4 \t"+ (payloadForSig BinarySig state)+ assertEqual+ "Canonical text signatures normalize line endings and trim trailing whitespace"+ "line1\r\nline2\r\nline3\r\nline4"+ (payloadForSig CanonicalTextSig state)++testTextNormalizationModes :: Assertion+testTextNormalizationModes = do+ let state =+ emptyPSC+ { lastLD =+ LiteralDataPkt+ TextData+ BL.empty+ (ThirtyTwoBitTimeStamp 0)+ "line1 \t\nline2\t \r\nline3"+ }+ let cleartextResult = payloadForSigWith CleartextCompat CanonicalTextSig state+ strictResult = payloadForSigWith RFC9580Strict CanonicalTextSig state+ assertEqual+ "CleartextCompat mode strips trailing whitespace per line"+ "line1\r\nline2\r\nline3"+ cleartextResult+ assertEqual+ "RFC9580Strict mode preserves trailing whitespace (CRLF-only normalization)"+ "line1 \t\r\nline2\t \r\nline3"+ strictResult+ (signer, signingKey) <- loadUnencryptedRsaSigner+ issuerKeyId <-+ case eightOctetKeyID signer of+ Left err ->+ assertFailure ("failed to derive issuer key id: " ++ err)+ >> fail "expected issuer key id"+ Right i -> pure i+ let payload = "line with trailing space \nno trailing space\n"+ hashed =+ [ SigSubPacket+ False+ (IssuerFingerprint IssuerFingerprintV4 (fingerprint signer))+ ]+ unhashed = [SigSubPacket False (Issuer issuerKeyId)]+ builderCompat = sigBuilderInit @'PKA.RSA CanonicalTextSig SHA512+ builderStrict =+ (sigBuilderInit @'PKA.RSA CanonicalTextSig SHA512)+ { sbTextNormMode = RFC9580Strict+ }+ withSubs b =+ addUnhashedSubs+ (listToUnhashedSubs unhashed)+ (addHashedSubs (listToHashedSubs hashed) b)+ bc = withSubs builderCompat+ bs = withSubs builderStrict+ sigCompat <- case signDataWithRSABuilder bc signingKey payload of+ Left err ->+ assertFailure+ ("CleartextCompat sign failed: " ++ renderSignError err)+ >> fail ""+ Right s -> pure s+ sigStrict <- case signDataWithRSABuilder bs signingKey payload of+ Left err ->+ assertFailure+ ("RFC9580Strict sign failed: " ++ renderSignError err)+ >> fail ""+ Right s -> pure s+ assertBool+ "CleartextCompat and RFC9580Strict produce different signatures for payloads with trailing whitespace"+ (sigCompat /= sigStrict)++testCanonicalTextSignatureSigningPaths :: Assertion+testCanonicalTextSignatureSigningPaths = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ issuerKeyId <-+ case eightOctetKeyID signer of+ Left err ->+ assertFailure ("failed to derive issuer key id: " ++ err)+ >> fail "expected issuer key id"+ Right i -> pure i+ let mixedPayload = "line1 \t\nline2\t \rline3\t \r\nline4 \t"+ normalizedPayload = "line1\r\nline2\r\nline3\r\nline4"+ hashed =+ [ SigSubPacket+ False+ (IssuerFingerprint IssuerFingerprintV4 (fingerprint signer))+ ]+ unhashed = [SigSubPacket False (Issuer issuerKeyId)]+ primitiveMixed <-+ case signDataWithRSA+ CanonicalTextSig+ signingKey+ hashed+ unhashed+ mixedPayload of+ Left err ->+ assertFailure+ ( "canonical text primitive signing failed: "+ ++ renderSignError err+ )+ >> fail (renderSignError err)+ Right sig -> pure sig+ primitiveNormalized <-+ case signDataWithRSA+ CanonicalTextSig+ signingKey+ hashed+ unhashed+ normalizedPayload of+ Left err ->+ assertFailure+ ( "canonical text primitive signing (normalized payload) failed: "+ ++ renderSignError err+ )+ >> fail (renderSignError err)+ Right sig -> pure sig+ assertEqual+ "primitive canonical text signing should normalize mixed line endings"+ primitiveNormalized+ primitiveMixed++ let builder = sigBuilderInit @'PKA.RSA CanonicalTextSig SHA512+ withHashed = addHashedSubs (listToHashedSubs hashed) builder+ withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed+ builderMixed <-+ case signDataWithRSABuilder withUnhashed signingKey mixedPayload of+ Left err ->+ assertFailure+ ("canonical text builder signing failed: " ++ renderSignError err)+ >> fail (renderSignError err)+ Right sig -> pure sig+ builderNormalized <-+ case signDataWithRSABuilder withUnhashed signingKey normalizedPayload of+ Left err ->+ assertFailure+ ( "canonical text builder signing (normalized payload) failed: "+ ++ renderSignError err+ )+ >> fail (renderSignError err)+ Right sig -> pure sig+ assertEqual+ "builder canonical text signing should normalize mixed line endings"+ builderNormalized+ builderMixed++testSignaturePrimitives :: Assertion+testSignaturePrimitives = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let userId = UserId "primitive-api@example.org"+ assertSigType+ :: String+ -> SigType+ -> Either SignError SignaturePayload+ -> Assertion+ assertSigType testLabel expected result =+ case result of+ Left err ->+ assertFailure (testLabel ++ " failed: " ++ renderSignError err)+ Right (SigV4 sigType _ _ _ _ _ _) ->+ assertEqual+ (testLabel ++ " uses expected signature type")+ expected+ sigType+ Right _ ->+ assertFailure+ (testLabel ++ " should generate a V4 signature payload")+ assertSigType+ "certification signature"+ GenericCert+ ( signCertificationWithRSA+ GenericCert+ signer+ userId+ []+ []+ signingKey+ )+ assertSigType+ "key revocation signature"+ KeyRevocationSig+ (signKeyRevocationWithRSA signer [] [] signingKey)+ assertSigType+ "subkey revocation signature"+ SubkeyRevocationSig+ (signSubkeyRevocationWithRSA signer signer [] [] signingKey)+ assertSigType+ "certification revocation signature"+ CertRevocationSig+ (signCertRevocationWithRSA signer userId [] [] signingKey)+ let left16Payload = "left16 primitive payload"+ left16Keyring = mkTestKeyring [TKUnknown (signer, Nothing) [] [] [] []]+ left16Sig <-+ case signDataWithRSA BinarySig signingKey [] [] left16Payload of+ Left err ->+ assertFailure+ ( "RSA primitive signing for left16 failed: "+ ++ renderSignError err+ )+ >> fail "expected RSA signature payload"+ Right sigPayload -> pure sigPayload+ case verifyAgainstKeyring+ left16Keyring+ (SignaturePkt left16Sig)+ Nothing+ left16Payload of+ Right _ -> pure ()+ Left err ->+ assertFailure+ ( "fresh RSA signature should verify with matching left16, got: "+ ++ renderVerificationError err+ )+ let tamperedLeft16Sig =+ case left16Sig of+ SigV4 st pka ha hs us l16 mpis -> SigV4 st pka ha hs us (l16 + 1) mpis+ other -> other+ case verifyAgainstKeyring+ left16Keyring+ (SignaturePkt tamperedLeft16Sig)+ Nothing+ left16Payload of+ Left _ -> pure ()+ Right _ ->+ assertFailure+ "verification unexpectedly succeeded with tampered left16"+ if isRight+ ( signCertificationWithRSA+ KeyRevocationSig+ signer+ userId+ []+ []+ signingKey+ )+ then+ assertFailure+ "certification primitive should reject non-certification signature types"+ else pure ()+ (edSigner, edSigningKey) <- loadDeterministicEd25519Signer+ edIssuerKeyId <-+ case eightOctetKeyID edSigner of+ Left err ->+ assertFailure ("failed to derive Ed25519 issuer key id: " ++ err)+ >> fail "expected Ed25519 issuer key id"+ Right i -> pure i+ let edPayload = "primitive Ed25519 payload"+ edHashed =+ [ SigSubPacket+ False+ (IssuerFingerprint IssuerFingerprintV4 (fingerprint edSigner))+ ]+ edUnhashed = [SigSubPacket False (Issuer edIssuerKeyId)]+ case signDataWithEd25519+ BinarySig+ edSigningKey+ edHashed+ edUnhashed+ edPayload of+ Left err ->+ assertFailure+ ("Ed25519 primitive signing failed: " ++ renderSignError err)+ Right (SigV4 BinarySig PKA.Ed25519 SHA512 _ _ _ _) -> pure ()+ Right other ->+ assertFailure+ ( "Ed25519 primitive should generate an Ed25519 SigV4 payload, got "+ ++ show other+ )+ let v6Salt = SignatureSalt (BL.replicate 32 0x42)+ case signDataWithEd25519V6+ BinarySig+ v6Salt+ edSigningKey+ edHashed+ edUnhashed+ edPayload of+ Left err ->+ assertFailure+ ("Ed25519 SigV6 primitive signing failed: " ++ renderSignError err)+ Right (SigV6 BinarySig PKA.Ed25519 SHA512 salt _ _ _ _) ->+ assertEqual+ "Ed25519 SigV6 primitive should preserve 32-byte salt"+ 32+ (BL.length (unSignatureSalt salt))+ Right other ->+ assertFailure+ ( "Ed25519 SigV6 primitive should generate an Ed25519 SigV6 payload, got "+ ++ show other+ )+ (ed448Signer, ed448SigningKey) <- loadDeterministicEd448Signer+ ed448IssuerKeyId <-+ case eightOctetKeyID ed448Signer of+ Left err ->+ assertFailure ("failed to derive Ed448 issuer key id: " ++ err)+ >> fail "expected Ed448 issuer key id"+ Right i -> pure i+ let ed448Payload = "primitive Ed448 payload"+ ed448Hashed =+ [ SigSubPacket+ False+ (IssuerFingerprint IssuerFingerprintV4 (fingerprint ed448Signer))+ ]+ ed448Unhashed = [SigSubPacket False (Issuer ed448IssuerKeyId)]+ case signDataWithEd448+ BinarySig+ ed448SigningKey+ ed448Hashed+ ed448Unhashed+ ed448Payload of+ Left err ->+ assertFailure+ ("Ed448 primitive signing failed: " ++ renderSignError err)+ Right (SigV4 BinarySig PKA.Ed448 SHA512 _ _ _ _) -> pure ()+ Right other ->+ assertFailure+ ( "Ed448 primitive should generate an Ed448 SigV4 payload, got "+ ++ show other+ )+ case signDataWithEd448V6+ BinarySig+ v6Salt+ ed448SigningKey+ ed448Hashed+ ed448Unhashed+ ed448Payload of+ Left err ->+ assertFailure+ ("Ed448 SigV6 primitive signing failed: " ++ renderSignError err)+ Right (SigV6 BinarySig PKA.Ed448 SHA512 salt _ _ _ _) ->+ assertEqual+ "Ed448 SigV6 primitive should preserve 32-byte salt"+ 32+ (BL.length (unSignatureSalt salt))+ Right other ->+ assertFailure+ ( "Ed448 SigV6 primitive should generate an Ed448 SigV6 payload, got "+ ++ show other+ )++testSignatureDataKindsCoercions :: Assertion+testSignatureDataKindsCoercions = do+ let sigV3 =+ SigV3+ BinarySig+ (ThirtyTwoBitTimeStamp 1)+ (EightOctetKeyId "\x01\x02\x03\x04\x05\x06\x07\x08")+ RSA+ SHA256+ 0+ (NE.fromList [MPI 1])+ sigV4 =+ SigV4+ BinarySig+ RSA+ SHA256+ []+ []+ 0+ (NE.fromList [MPI 2])+ sigV6 =+ SigV6+ BinarySig+ PKA.Ed25519+ SHA512+ (SignatureSalt (BL.replicate 32 0x01))+ []+ []+ 0+ (NE.fromList [MPI 3, MPI 4])+ sigOther = SigVOther 77 "opaque-signature-body"+ assertPacketRoundTrip label expected pkt =+ case fromPktEitherSomeSignatureV pkt of+ Left err -> assertFailure (label ++ " packet coercion failed: " ++ err)+ Right (SomeSignatureV typedSig) ->+ assertEqual+ (label ++ " packet coercion preserves payload")+ expected+ (signaturePayloadFromSignatureV typedSig)+ assertBool+ "toSomeSignaturePayload should preserve SigV3 witness"+ (isRight (asSignaturePayloadV3 sigV3))+ assertBool+ "toSomeSignaturePayload should preserve SigV4 witness"+ (isRight (asSignaturePayloadV4 sigV4))+ assertBool+ "toSomeSignaturePayload should preserve SigV6 witness"+ (isRight (asSignaturePayloadV6 sigV6))+ assertBool+ "toSomeSignaturePayload should preserve SigVOther witness"+ (isRight (asSignaturePayloadOther sigOther))+ case asSignaturePayloadV3 sigV3 of+ Left err ->+ assertFailure+ ("asSignaturePayloadV3 should accept SigV3: " ++ err)+ Right typed ->+ assertEqual+ "asSignaturePayloadV3 round-trips SigV3"+ sigV3+ (toSignaturePayload typed)+ case asSignaturePayloadV4 sigV4 of+ Left err ->+ assertFailure+ ("asSignaturePayloadV4 should accept SigV4: " ++ err)+ Right typed ->+ assertEqual+ "asSignaturePayloadV4 round-trips SigV4"+ sigV4+ (toSignaturePayload typed)+ case asSignaturePayloadV6 sigV6 of+ Left err ->+ assertFailure+ ("asSignaturePayloadV6 should accept SigV6: " ++ err)+ Right typed ->+ assertEqual+ "asSignaturePayloadV6 round-trips SigV6"+ sigV6+ (toSignaturePayload typed)+ case asSignaturePayloadOther sigOther of+ Left err ->+ assertFailure+ ("asSignaturePayloadOther should accept SigVOther: " ++ err)+ Right typed ->+ assertEqual+ "asSignaturePayloadOther round-trips SigVOther"+ sigOther+ (toSignaturePayload typed)+ assertBool+ "asSignaturePayloadV6 rejects SigV4"+ (not (isRight (asSignaturePayloadV6 sigV4)))+ assertBool+ "asSignaturePayloadV4 rejects SigVOther"+ (not (isRight (asSignaturePayloadV4 sigOther)))+ assertPacketRoundTrip "SigV3" sigV3 (SignaturePkt sigV3)+ assertPacketRoundTrip "SigV4" sigV4 (SignaturePkt sigV4)+ assertPacketRoundTrip "SigV6" sigV6 (SignaturePkt sigV6)+ assertPacketRoundTrip+ "SigVOther"+ sigOther+ (SignaturePkt sigOther)+ assertBool+ "fromPktEitherSomeSignatureV rejects non-signature packets"+ ( not+ ( isRight+ (fromPktEitherSomeSignatureV (LiteralDataPkt BinaryData "" 0 ""))+ )+ )++testTypedArmorPayloadHelpers :: Assertion+testTypedArmorPayloadHelpers = do+ let armors =+ [ Armor ArmorPublicKeyBlock [] "public-one"+ , Armor ArmorMessage [] "message-one"+ , Armor ArmorPublicKeyBlock [] "public-two"+ ]+ publicPayloads =+ map BL.toStrict (armorPayloadsOfType ArmorPublicKeyBlock armors)+ assertEqual+ "armorPayloadsOfType returns all matching typed blocks in order"+ ["public-one", "public-two"]+ publicPayloads+ assertEqual+ "singleArmorPayloadOfType returns the sole matching message payload"+ (Right "message-one")+ (fmap BL.toStrict (singleArmorPayloadOfType ArmorMessage armors))+ assertBool+ "singleArmorPayloadOfType fails when no typed blocks exist"+ (isLeft (singleArmorPayloadOfType ArmorPrivateKeyBlock armors))+ assertBool+ "singleArmorPayloadOfType fails when multiple typed blocks exist"+ (isLeft (singleArmorPayloadOfType ArmorPublicKeyBlock armors))+testRecommendedArmorType :: Assertion+testRecommendedArmorType = do+ secretArmors <- loadArmor "v4-encrypted-secret.pgp.aa"+ publicArmors <- loadArmor "v4-encrypted.rev.aa"+ secretPayload <-+ case singleArmorPayloadOfType ArmorPrivateKeyBlock secretArmors of+ Left err -> assertFailure err >> fail err+ Right payload -> pure payload+ publicPayload <-+ case singleArmorPayloadOfType ArmorPublicKeyBlock publicArmors of+ Left err -> assertFailure err >> fail err+ Right payload -> pure payload+ let secretPkts = parsePkts secretPayload+ publicPkts = parsePkts publicPayload+ firstPkt label pkts =+ case pkts of+ [] ->+ assertFailure (label ++ " should contain at least one packet")+ >> fail "missing packet"+ pkt : _ -> pure pkt+ firstSecret <- firstPkt "secret armor fixture" secretPkts+ firstPublic <- firstPkt "public armor fixture" publicPkts+ let sig =+ SignaturePkt+ (SigV4 BinarySig RSA SHA256 [] [] 0 (NE.fromList [MPI 1]))+ assertEqual+ "recommendedArmorType rejects empty packet streams"+ Nothing+ (recommendedArmorType [])+ assertEqual+ "recommendedArmorType maps secret-key packets to private key armor"+ (Just ArmorPrivateKeyBlock)+ (recommendedArmorType [firstSecret])+ assertEqual+ "recommendedArmorType maps public-key packets to public key armor"+ (Just ArmorPublicKeyBlock)+ (recommendedArmorType [firstPublic])+ assertEqual+ "recommendedArmorType maps signature packets to signature armor"+ (Just ArmorSignature)+ (recommendedArmorType [sig])+ assertEqual+ "recommendedArmorType defaults non-key/signature packets to message armor"+ (Just ArmorMessage)+ (recommendedArmorType [LiteralDataPkt BinaryData "" 0 "payload"])+testSingleClearSignedBlock :: Assertion+testSingleClearSignedBlock = do+ let clearSigned =+ ClearSigned+ [("Hash", "SHA256")]+ "signed cleartext payload\n"+ ( Armor+ ArmorSignature+ [("Version", "test-suite")]+ "detached-signature"+ )+ assertEqual+ "singleClearSignedBlock extracts cleartext and signature payloads"+ ( Right+ ( [("Hash", "SHA256")]+ , "signed cleartext payload\n"+ , "detached-signature"+ )+ )+ (singleClearSignedBlock [clearSigned])+ assertBool+ "singleClearSignedBlock rejects absent clear-signed blocks"+ (isLeft (singleClearSignedBlock [Armor ArmorMessage [] "payload"]))+ assertBool+ "singleClearSignedBlock rejects multiple clear-signed blocks"+ (isLeft (singleClearSignedBlock [clearSigned, clearSigned]))+ assertBool+ "singleClearSignedBlock rejects non-signature inner armor blocks"+ ( isLeft+ ( singleClearSignedBlock+ [ ClearSigned [] "payload" (Armor ArmorMessage [] "not-signature")+ ]+ )+ )+testSignerTimelineSoftPrimaryRevocation :: Assertion+testSignerTimelineSoftPrimaryRevocation = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let payload = "soft primary revocation timeline payload"+ keyCreated = _timestamp signer+ sigBeforeTime = addTimestampSeconds keyCreated 10+ revocationTime = addTimestampSeconds keyCreated 20+ sigAfterTime = addTimestampSeconds keyCreated 30+ sigBefore <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ sigBeforeTime+ payload+ revocation <-+ signKeyRevocationWithReasonAt+ signer+ signingKey+ revocationTime+ KeySuperseded+ sigAfter <-+ signBinaryMessageWithRSAAt signer signingKey sigAfterTime payload+ let keyring =+ mkTestKeyring [TKUnknown (signer, Nothing) [revocation] [] [] []]+ assertSingleSignerFingerprint+ "soft primary revocation should preserve pre-revocation signatures"+ (fingerprint signer)+ (verifyTimelinePackets keyring payload sigBefore)+ assertSingleFailureContainsTimeline+ "soft primary revocation should reject post-revocation signatures"+ "signing key is revoked"+ (verifyTimelinePackets keyring payload sigAfter)++testSignerTimelineHardPrimaryRevocation :: Assertion+testSignerTimelineHardPrimaryRevocation = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let payload = "hard primary revocation timeline payload"+ keyCreated = _timestamp signer+ sigBeforeTime = addTimestampSeconds keyCreated 10+ revocationTime = addTimestampSeconds keyCreated 20+ sigBefore <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ sigBeforeTime+ payload+ revocation <-+ signKeyRevocationWithReasonAt+ signer+ signingKey+ revocationTime+ KeyMaterialCompromised+ let keyring =+ mkTestKeyring [TKUnknown (signer, Nothing) [revocation] [] [] []]+ assertSingleFailureContainsTimeline+ "hard primary revocation should reject even pre-revocation signatures"+ "signing key is revoked"+ (verifyTimelinePackets keyring payload sigBefore)++testSignerTimelineNoReasonPrimaryRevocation :: Assertion+testSignerTimelineNoReasonPrimaryRevocation = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let payload = "no-reason primary revocation timeline payload"+ keyCreated = _timestamp signer+ sigBeforeTime = addTimestampSeconds keyCreated 10+ revocationTime = addTimestampSeconds keyCreated 20+ sigBefore <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ sigBeforeTime+ payload+ revocation <-+ signKeyRevocationWithReasonAt+ signer+ signingKey+ revocationTime+ NoReason+ let keyring =+ mkTestKeyring [TKUnknown (signer, Nothing) [revocation] [] [] []]+ assertSingleFailureContainsTimeline+ "no-reason primary revocation should reject even pre-revocation signatures"+ "signing key is revoked"+ (verifyTimelinePackets keyring payload sigBefore)++testSignerTimelineUnknownReasonPrimaryRevocation :: Assertion+testSignerTimelineUnknownReasonPrimaryRevocation = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let payload = "unknown-reason primary revocation timeline payload"+ keyCreated = _timestamp signer+ sigBeforeTime = addTimestampSeconds keyCreated 10+ revocationTime = addTimestampSeconds keyCreated 20+ sigBefore <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ sigBeforeTime+ payload+ revocation <-+ signKeyRevocationWithReasonAt+ signer+ signingKey+ revocationTime+ (RCoOther 100)+ let keyring =+ mkTestKeyring [TKUnknown (signer, Nothing) [revocation] [] [] []]+ assertSingleFailureContainsTimeline+ "unknown-reason primary revocation should reject even pre-revocation signatures"+ "signing key is revoked"+ (verifyTimelinePackets keyring payload sigBefore)++testSignerTimelineNonCompromisePrimaryRevocationIsHistorical+ :: Assertion+testSignerTimelineNonCompromisePrimaryRevocationIsHistorical = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let payload = "non-compromise primary revocation timeline payload"+ keyCreated = _timestamp signer+ sigBeforeTime = addTimestampSeconds keyCreated 10+ revocationTime = addTimestampSeconds keyCreated 20+ sigAfterTime = addTimestampSeconds keyCreated 30+ sigBefore <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ sigBeforeTime+ payload+ revocation <-+ signKeyRevocationWithReasonAt+ signer+ signingKey+ revocationTime+ UserIdInfoNoLongerValid+ sigAfter <-+ signBinaryMessageWithRSAAt signer signingKey sigAfterTime payload+ let keyring =+ mkTestKeyring [TKUnknown (signer, Nothing) [revocation] [] [] []]+ assertSingleSignerFingerprint+ "non-compromise primary revocation should preserve pre-revocation signatures"+ (fingerprint signer)+ (verifyTimelinePackets keyring payload sigBefore)+ assertSingleFailureContainsTimeline+ "non-compromise primary revocation should reject post-revocation signatures"+ "signing key is revoked"+ (verifyTimelinePackets keyring payload sigAfter)++testSignerTimelineTemporaryPrimaryRevocationExpires :: Assertion+testSignerTimelineTemporaryPrimaryRevocationExpires = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let payload = "temporary primary revocation expiry timeline payload"+ keyCreated = _timestamp signer+ sigBeforeTime = addTimestampSeconds keyCreated 10+ revocationTime = addTimestampSeconds keyCreated 20+ sigDuringTime = addTimestampSeconds keyCreated 25+ sigAfterTime = addTimestampSeconds keyCreated 35+ sigBefore <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ sigBeforeTime+ payload+ revocation <-+ signKeyRevocationWithReasonAndExtrasAt+ signer+ signingKey+ revocationTime+ KeySuperseded+ [SigSubPacket False (SigExpirationTime 10)]+ sigDuring <-+ signBinaryMessageWithRSAAt+ signer+ signingKey+ sigDuringTime+ payload+ sigAfter <-+ signBinaryMessageWithRSAAt signer signingKey sigAfterTime payload+ let keyring =+ mkTestKeyring [TKUnknown (signer, Nothing) [revocation] [] [] []]+ assertSingleSignerFingerprint+ "temporary primary revocation should preserve pre-revocation signatures"+ (fingerprint signer)+ (verifyTimelinePackets keyring payload sigBefore)+ assertSingleFailureContainsTimeline+ "temporary primary revocation should reject signatures while revocation is effective"+ "signing key is revoked"+ (verifyTimelinePackets keyring payload sigDuring)+ assertSingleSignerFingerprint+ "temporary primary revocation should allow signatures after revocation expiration"+ (fingerprint signer)+ (verifyTimelinePackets keyring payload sigAfter)++testSignerTimelineSubkeyRevocation :: Assertion+testSignerTimelineSubkeyRevocation = do+ (primarySigner, primarySigningKey) <- loadUnencryptedRsaSigner+ (subkeySigner, subkeySigningKey) <-+ loadDeterministicEd25519Signer+ let payload = "subkey revocation timeline payload"+ keyCreated = _timestamp primarySigner+ bindingTime = addTimestampSeconds keyCreated 10+ sigBeforeTime = addTimestampSeconds keyCreated 20+ revocationTime = addTimestampSeconds keyCreated 30+ sigAfterTime = addTimestampSeconds keyCreated 40+ subkeyPacket = PublicSubkeyPkt subkeySigner+ bindingSig <-+ signSubkeyBindingWithRSAAt+ primarySigner+ subkeySigner+ primarySigningKey+ bindingTime+ sigBefore <-+ signBinaryMessageWithEd25519At+ subkeySigner+ subkeySigningKey+ sigBeforeTime+ payload+ revocationSig <-+ signSubkeyRevocationWithRSAAt+ primarySigner+ subkeySigner+ primarySigningKey+ revocationTime+ sigAfter <-+ signBinaryMessageWithEd25519At+ subkeySigner+ subkeySigningKey+ sigAfterTime+ payload+ let keyring =+ mkTestKeyring+ [ TKUnknown+ (primarySigner, Nothing)+ []+ []+ []+ [(subkeyPacket, [bindingSig, revocationSig])]+ ]+ assertSingleSignerFingerprint+ "subkey revocation should preserve pre-revocation signatures"+ (fingerprint subkeySigner)+ (verifyTimelinePackets keyring payload sigBefore)+ assertSingleFailureContainsTimeline+ "subkey revocation should reject post-revocation signatures"+ "signing key was not valid at the signature creation time"+ (verifyTimelinePackets keyring payload sigAfter)++testSignMessageConvenience :: Assertion+testSignMessageConvenience = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ signerV4 <- expectV4PKPayload "RSA v4 convenience signer" signer+ signedResult <-+ signMessage+ (mkRSASignerV4 signerV4 signingKey)+ "message-api convenience signing payload"+ signedMessage <-+ case signedResult of+ Left err ->+ assertFailure+ ("message signing convenience API failed: " ++ show err)+ >> pure mempty+ Right bs -> pure bs+ case parsePkts signedMessage of+ [LiteralDataPkt {}, SignaturePkt _] -> pure ()+ _ ->+ assertFailure+ "convenience signing output should contain literal data and one signature packet"++testTypedVerifySurfaceMatchesLegacy :: Assertion+testTypedVerifySurfaceMatchesLegacy = do+ kr <- loadKeyring "pubring.gpg"+ signedMessage <- readFixtureLazy "uncompressed-ops-rsa.gpg"+ let typedResults =+ verifyMessage+ defaultVerificationOptions+ { verificationPolicy = VerifyInformational+ , verificationMode = VerificationStreaming+ }+ kr+ signedMessage+ strictResults =+ verifyMessage+ defaultVerificationOptions+ { verificationPolicy = VerifyStrict+ , verificationMode = VerificationStreaming+ }+ kr+ signedMessage+ normalizeTyped+ :: Either VerificationError Verification -> Either Bool Fingerprint+ normalizeTyped (Left _) = Left False+ normalizeTyped (Right v) = Right (fingerprint (_verificationSigner v))+ assertEqual+ "strict verification should collapse informational failures"+ ( map+ normalizeTyped+ (either (pure . Left) (Right <$>) (sequence typedResults))+ )+ (map normalizeTyped strictResults)+ assertBool+ "informational verification should emit at least one result for known-good fixture"+ (not (null (map normalizeTyped typedResults)))++testVerifyMessageStrictRejectsTamper :: Assertion+testVerifyMessageStrictRejectsTamper = do+ kr <- loadKeyring "pubring.gpg"+ packets <- loadAndDecompressPkts "uncompressed-ops-rsa.gpg"+ let tamperedPackets = map tamperLiteral packets+ strictOptions =+ defaultVerificationOptions+ { verificationPolicy = VerifyStrict+ , verificationMode = VerificationStreaming+ }+ case verifyMessagePackets strictOptions kr tamperedPackets of+ [Left _] -> pure ()+ _ ->+ assertFailure+ "strict typed verification should fail on tampered payload"+ where+ tamperLiteral (LiteralDataPkt dt fn ts payload) =+ LiteralDataPkt dt fn ts (BL.snoc payload 0)+ tamperLiteral pkt = pkt++testVerifySignedMessageConvenience :: Assertion+testVerifySignedMessageConvenience = do+ kr <- loadKeyring "pubring.gpg"+ signedMessage <- readFixtureLazy "uncompressed-ops-rsa.gpg"+ strictResult <-+ pure+ ( verifySignedMessage+ defaultVerificationOptions+ { verificationPolicy = VerifyStrict+ , verificationMode = VerificationStreaming+ }+ kr+ signedMessage+ )+ case strictResult of+ [Left _] ->+ assertFailure+ "verifySignedMessage should succeed for known-good fixture"+ verifications ->+ assertBool+ "verifySignedMessage should emit at least one verification"+ (not (null verifications))++testVerifyV4EdDSASignatureWithEd25519KeyAlgorithm :: Assertion+testVerifyV4EdDSASignatureWithEd25519KeyAlgorithm = do+ (signer, signingKey) <- loadDeterministicEd25519Signer+ let signerWithEd25519Pka = setPKAlgorithm PKA.Ed25519 signer+ signerV4 <-+ expectV4PKPayload+ "Ed25519 v4 signer with Ed25519 key algorithm"+ signerWithEd25519Pka+ let payload = "v4 eddsa signature with Ed25519 key algorithm"+ state =+ emptyPSC {lastLD = LiteralDataPkt BinaryData BL.empty 0 payload}+ keyring =+ mkTestKeyring+ [TKUnknown (signerWithEd25519Pka, Nothing) [] [] [] []]+ signedResult <-+ signMessageWith+ (mkEd25519SignerV4 signerV4 signingKey)+ (mkClearPayload payload)+ signaturePkt <-+ case signedResult of+ Left err ->+ assertFailure ("Ed25519 message signing failed: " ++ show err)+ >> fail "expected signed Ed25519 payload"+ Right signedMessage ->+ case parsePkts signedMessage of+ [ LiteralDataPkt {}+ , sig@(SignaturePkt (SigV4 BinarySig PKA.Ed25519 SHA512 _ _ _ _))+ ] ->+ pure sig+ other ->+ assertFailure+ ( "Expected [LiteralDataPkt, Ed25519 SigV4] for Ed25519-key-algorithm verification test, got "+ ++ show other+ )+ >> fail "unexpected Ed25519-key-algorithm signature shape"+ case verifySigWith+ defaultVerificationPolicy+ (verifyAgainstKeyring keyring)+ signaturePkt+ state+ Nothing of+ Left err ->+ assertFailure+ ( "Ed25519 SigV4 verification with Ed25519 key algorithm failed: "+ ++ renderVerificationError err+ )+ Right _ -> pure ()
tests/Tests/Serialization.hs view
@@ -2,955 +2,1167 @@ -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).--{-# LANGUAGE OverloadedStrings #-}--module Tests.Serialization (serializationTests) where--import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)-import Codec.Encryption.OpenPGP.Internal (emptyPSC, point2MBS)-import Codec.Encryption.OpenPGP.KeyringParser (parseTKsWithWireRep)-import Codec.Encryption.OpenPGP.Policy- ( signatureV6SaltSizeForHashAlgorithm- )-import Codec.Encryption.OpenPGP.Serialize (parsePkts, parsePktsWithWireRep)-import Codec.Encryption.OpenPGP.Signatures- ( VerificationError(..)- , verifySigWith- )-import Codec.Encryption.OpenPGP.Types-import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as PKA-import Control.Applicative ((<|>))-import Control.Monad (forM_)-import Crypto.Number.Serialize (os2ip)-import Data.Binary (Get, get, put)-import Data.Binary.Put (putByteString, putWord16be, putWord32be, putWord8, runPut)-import Data.Bits (xor)-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import qualified Crypto.PubKey.ECC.Types as ECCT-import qualified Crypto.PubKey.ECC.ECDSA as ECDSA-import Data.Conduit.Serialization.Binary (conduitGet)-import qualified Data.Conduit as DC-import qualified Data.Conduit.Binary as CB-import qualified Data.Conduit.List as CL-import qualified Data.List.NonEmpty as NE-import Data.List.NonEmpty (NonEmpty(..))-import Data.Maybe (listToMaybe)-import qualified Data.Set as Set-import Data.Word (Word8)-import Test.Tasty (TestTree, testGroup)-import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, testCase)-import Tests.Common- ( armorPayload- , loadArmor- , readFixturePayload- , runGet- )--serializationTests :: TestTree-serializationTests =- testGroup- "Serialization"- [ testGroup- "Serialization group"- [ testCase- "000001-006.public_key"- (testSerialization "000001-006.public_key")- , testCase- "issuer-fingerprint-rejects-unknown-version"- testIssuerFingerprintRejectsUnknownVersion- , testCase- "v6-ed25519-public-key-serializes-fixed-length"- testV6Ed25519PublicKeySerializesFixedLength- , testCase- "v4-ed25519-public-key-parses-native-fixed-length"- testV4Ed25519PublicKeyParsesNativeFixedLength- , testCase- "v4-x25519-public-subkey-parses-native-fixed-length"- testV4X25519PublicSubkeyParsesNativeFixedLength- , testCase- "v6-signature-issuer-fingerprint-version-mismatch"- testV6SignatureIssuerFingerprintVersionMismatch- , testCase "000002-013.user_id" (testSerialization "000002-013.user_id")- , testCase "000003-002.sig" (testSerialization "000003-002.sig")- , testCase- "000004-012.ring_trust"- (testSerialization "000004-012.ring_trust")- , testCase "000005-002.sig" (testSerialization "000005-002.sig")- , testCase- "000006-012.ring_trust"- (testSerialization "000006-012.ring_trust")- , testCase "000007-002.sig" (testSerialization "000007-002.sig")- , testCase- "000008-012.ring_trust"- (testSerialization "000008-012.ring_trust")- , testCase "000009-002.sig" (testSerialization "000009-002.sig")- , testCase- "000010-012.ring_trust"- (testSerialization "000010-012.ring_trust")- , testCase "000011-002.sig" (testSerialization "000011-002.sig")- , testCase- "000012-012.ring_trust"- (testSerialization "000012-012.ring_trust")- , testCase- "000013-014.public_subkey"- (testSerialization "000013-014.public_subkey")- , testCase "000014-002.sig" (testSerialization "000014-002.sig")- , testCase- "000015-012.ring_trust"- (testSerialization "000015-012.ring_trust")- , testCase- "000016-006.public_key"- (testSerialization "000016-006.public_key")- , testCase "000017-002.sig" (testSerialization "000017-002.sig")- , testCase- "000018-012.ring_trust"- (testSerialization "000018-012.ring_trust")- , testCase "000019-013.user_id" (testSerialization "000019-013.user_id")- , testCase "000020-002.sig" (testSerialization "000020-002.sig")- , testCase- "000021-012.ring_trust"- (testSerialization "000021-012.ring_trust")- , testCase "000022-002.sig" (testSerialization "000022-002.sig")- , testCase- "000023-012.ring_trust"- (testSerialization "000023-012.ring_trust")- , testCase- "000024-014.public_subkey"- (testSerialization "000024-014.public_subkey")- , testCase "000025-002.sig" (testSerialization "000025-002.sig")- , testCase- "000026-012.ring_trust"- (testSerialization "000026-012.ring_trust")- , testCase- "000027-006.public_key"- (testSerialization "000027-006.public_key")- , testCase "000028-002.sig" (testSerialization "000028-002.sig")- , testCase- "000029-012.ring_trust"- (testSerialization "000029-012.ring_trust")- , testCase "000030-013.user_id" (testSerialization "000030-013.user_id")- , testCase "000031-002.sig" (testSerialization "000031-002.sig")- , testCase- "000032-012.ring_trust"- (testSerialization "000032-012.ring_trust")- , testCase "000033-002.sig" (testSerialization "000033-002.sig")- , testCase- "000034-012.ring_trust"- (testSerialization "000034-012.ring_trust")- , testCase- "000035-006.public_key"- (testSerialization "000035-006.public_key")- , testCase "000036-013.user_id" (testSerialization "000036-013.user_id")- , testCase "000037-002.sig" (testSerialization "000037-002.sig")- , testCase- "000038-012.ring_trust"- (testSerialization "000038-012.ring_trust")- , testCase "000039-002.sig" (testSerialization "000039-002.sig")- , testCase- "000040-012.ring_trust"- (testSerialization "000040-012.ring_trust")- , testCase- "000041-017.attribute"- (testSerialization "000041-017.attribute")- , testCase "000042-002.sig" (testSerialization "000042-002.sig")- , testCase- "000043-012.ring_trust"- (testSerialization "000043-012.ring_trust")- , testCase- "000044-014.public_subkey"- (testSerialization "000044-014.public_subkey")- , testCase "000045-002.sig" (testSerialization "000045-002.sig")- , testCase- "000046-012.ring_trust"- (testSerialization "000046-012.ring_trust")- , testCase- "000047-005.secret_key"- (testSerialization "000047-005.secret_key")- , testCase "000048-013.user_id" (testSerialization "000048-013.user_id")- , testCase "000049-002.sig" (testSerialization "000049-002.sig")- , testCase- "000050-012.ring_trust"- (testSerialization "000050-012.ring_trust")- , testCase- "000051-007.secret_subkey"- (testSerialization "000051-007.secret_subkey")- , testCase "000052-002.sig" (testSerialization "000052-002.sig")- , testCase- "000053-012.ring_trust"- (testSerialization "000053-012.ring_trust")- , testCase- "000054-005.secret_key"- (testSerialization "000054-005.secret_key")- , testCase "000055-002.sig" (testSerialization "000055-002.sig")- , testCase- "000056-012.ring_trust"- (testSerialization "000056-012.ring_trust")- , testCase "000057-013.user_id" (testSerialization "000057-013.user_id")- , testCase "000058-002.sig" (testSerialization "000058-002.sig")- , testCase- "000059-012.ring_trust"- (testSerialization "000059-012.ring_trust")- , testCase- "000060-007.secret_subkey"- (testSerialization "000060-007.secret_subkey")- , testCase "000061-002.sig" (testSerialization "000061-002.sig")- , testCase- "000062-012.ring_trust"- (testSerialization "000062-012.ring_trust")- , testCase- "000063-005.secret_key"- (testSerialization "000063-005.secret_key")- , testCase "000064-002.sig" (testSerialization "000064-002.sig")- , testCase- "000065-012.ring_trust"- (testSerialization "000065-012.ring_trust")- , testCase "000066-013.user_id" (testSerialization "000066-013.user_id")- , testCase "000067-002.sig" (testSerialization "000067-002.sig")- , testCase- "000068-012.ring_trust"- (testSerialization "000068-012.ring_trust")- , testCase- "000069-005.secret_key"- (testSerialization "000069-005.secret_key")- , testCase "000070-013.user_id" (testSerialization "000070-013.user_id")- , testCase "000071-002.sig" (testSerialization "000071-002.sig")- , testCase- "000072-012.ring_trust"- (testSerialization "000072-012.ring_trust")- , testCase- "000073-017.attribute"- (testSerialization "000073-017.attribute")- , testCase "000074-002.sig" (testSerialization "000074-002.sig")- , testCase- "000075-012.ring_trust"- (testSerialization "000075-012.ring_trust")- , testCase- "000076-007.secret_subkey"- (testSerialization "000076-007.secret_subkey")- , testCase "000077-002.sig" (testSerialization "000077-002.sig")- , testCase- "000078-012.ring_trust"- (testSerialization "000078-012.ring_trust")- , testCase "pubring.gpg" (testSerialization "pubring.gpg")- , testCase "secring.gpg" (testSerialization "secring.gpg")- , testCase "compressedsig.gpg" (testSerialization "compressedsig.gpg")- , testCase- "compressedsig-zlib.gpg"- (testSerialization "compressedsig-zlib.gpg")- , testCase- "compressedsig-bzip2.gpg"- (testSerialization "compressedsig-bzip2.gpg")- , testCase "onepass_sig" (testSerialization "onepass_sig")- , testCase- "uncompressed-ops-dsa.gpg"- (testSerialization "uncompressed-ops-dsa.gpg")- , testCase- "uncompressed-ops-rsa.gpg"- (testSerialization "uncompressed-ops-rsa.gpg")- , testCase "simple.seckey" (testSerialization "simple.seckey")- , testCase "v3-genericcert.sig" (testSerialization "v3-genericcert.sig")- , testCase "sigs-with-regexes" (testSerialization "sigs-with-regexes")- , testCase- "gnu-dummy-s2k-101-secret-key.gpg"- (testSerialization "gnu-dummy-s2k-101-secret-key.gpg")- , testCase "anibal-ed25519.gpg" (testSerialization "anibal-ed25519.gpg")- , testCase "nist_p-256_key.gpg" (testSerialization "nist_p-256_key.gpg")- , testCase- "nist_p-256_secretkey.gpg"- (testSerialization "nist_p-256_secretkey.gpg")- , testCase "v6-secret.pgp.aa" (testSerialization "v6-secret.pgp.aa")- , testCase- "sample-eddsa.pubkey"- (testSerialization "sample-eddsa.pubkey")- , testCase "should not serialize point at infinity" testPointAtInfinitySerialization- , testCase- "should reject mismatched EC coordinate widths"- testPointSerializationRejectsMismatchedCoordinateWidths- ]- , testGroup- "TKUnknown Serialization group"- [ testCase "pubring.gpg TKUnknown serialization" (testTKSerialization "pubring.gpg")- , testCase "secring.gpg TKUnknown serialization" (testTKSerialization "secring.gpg")- ]- , testGroup- "Argon2 S2K group"- [ testCase "Argon2 SKESK packet roundtrip" testArgon2S2KPacketRoundTrip- ]- , testGroup- "RFC9580 SEIPD v2 group"- [ testCase "SEIPD v2 packet roundtrip" testSEIPDv2PacketRoundTrip- , testCase- "SEIPD v2 rejects invalid chunk size"- testSEIPDv2RejectInvalidChunkSize- , testCase- "PKESKv6 parsing does not fall back to legacy parser"- testPKESKv6ParsesAsV6WithoutLegacyFallback- , testCase- "PKESKv6 rejects invalid recipient key version"- testPKESKv6RejectsInvalidRecipientIdentifierVersion- , testCase- "PKESKv6 rejects recipient length/version mismatches"- testPKESKv6RejectsRecipientIdentifierLengthVersionMismatch- , testCase- "legacy RSA PKESK rejects extra MPIs"- testLegacyPKESKRSARejectsExtraMPI- , testCase- "legacy ECDH PKESK rejects wrong MPI count"- testLegacyPKESKECDHRejectsWrongMPICount- , testCase- "legacy X25519 PKESK rejects wrong MPI count"- testLegacyPKESKX25519RejectsWrongMPICount- , testCase- "legacy unencrypted secret key rejects checksum mismatch"- testLegacyUnencryptedSecretKeyRejectsChecksumMismatch- , testCase- "legacy secret key rejects unsupported symmetric algorithm IV sizing"- testLegacySecretKeyRejectsUnsupportedSymmetricAlgorithmIVSizing- , testCase- "legacy X25519 PKESK parses RFC9580 v3 octet layout"- testLegacyPKESKX25519ParsesRFC9580V3OctetLayout- , testCase "SigV6 rejects invalid salt size" testSigV6RejectsInvalidSaltSize- , testCase "OPS6 rejects invalid salt size" testOPS6RejectsInvalidSaltSize- , testCase "OPS3 rejects invalid nested-flag octet" testOPS3RejectsInvalidNestedFlagOctet- , testCase "OPS6 rejects invalid nested-flag octet" testOPS6RejectsInvalidNestedFlagOctet- , testCase- "ECDH pubkey rejects reserved KDF length 0"- testECDHPubkeyRejectsReservedKDFLengthZero- , testCase- "ECDH pubkey rejects reserved KDF length 255"- testECDHPubkeyRejectsReservedKDFLength255- , testCase- "ECDH pubkey encodes fixed KDF length trailer"- testECDHPubkeyEncodesFixedKDFLength- , testCase- "empty key-flags subpacket encodes explicit zero octet"- testKeyFlagsSubpacketEncodesEmptySetWithZeroOctet- , testCase- "v6-secret fixture SigV6 semantics"- testV6SecretFixtureSignatureSemantics- , testCase- "v6-secret fixture derives eight-octet key-id from fingerprint prefix"- testV6SecretFixtureDerivesEightOctetKeyID- ]- ]--testSerialization :: FilePath -> Assertion-testSerialization fpr = do- bs <- readFixturePayload fpr- let firstpass = runGet get bs- case fmap unBlock firstpass of- Left _ -> assertFailure $ "First pass failed on " ++ fpr- Right [] ->- assertFailure $ "First pass of " ++ fpr ++ " decoded to nothing."- Right packs -> do- let roundtrip = runPut $ put (Block packs)- let secondpass = runGet (get :: Get (Block Pkt)) roundtrip- if fmap unBlock secondpass == Right []- then assertFailure $ "Second pass of " ++ fpr ++ " decoded to nothing."- else assertEqual ("for " ++ fpr) firstpass secondpass--testTKSerialization :: FilePath -> Assertion-testTKSerialization fpr = do- bs <- readFixturePayload fpr- let pkts = parsePktsWithWireRep (wireRepRef bs) bs- tksWithWireRep = parseTKsWithWireRep True pkts- if null tksWithWireRep- then assertFailure $ "TKUnknown serialization test: " ++ fpr ++ " parsed to no TKs"- else forM_ tksWithWireRep (testTKRoundtrip fpr)--testTKRoundtrip :: FilePath -> TKWithWireRep -> Assertion-testTKRoundtrip fpr tk = do- let packets = _tkPackets tk- encoded = runPut (put (Block (map _pktValue packets)))- case runGet (get :: Get (Block Pkt)) encoded of- Left err -> assertFailure $ "TKUnknown " ++ fpr ++ " packet re-parse failed: " ++ err- Right reparsedBlock ->- assertEqual- ("TKUnknown packet re-serialization roundtrip for " ++ fpr)- (Block (map _pktValue packets))- reparsedBlock- case toStructuredTKWithWireRep tk of- Left err -> assertFailure $ "TKUnknown structured conversion failed for " ++ fpr ++ ": " ++ show err- Right structured ->- case canonicalizeTKStructuredWithWireRep structured of- Left err -> assertFailure $ "TKUnknown canonical conversion failed for " ++ fpr ++ ": " ++ show err- Right _canonical -> pure ()--testArgon2S2KPacketRoundTrip :: Assertion-testArgon2S2KPacketRoundTrip = do- let s2k = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15- pkt = SKESKPkt (SKESKPayloadV4Packet (SKESKPayloadV4 AES128 s2k Nothing))- encoded = runPut (put pkt)- assertEqual- "Argon2 S2K SKESK packet roundtrip"- (Right pkt)- (runGet (get :: Get Pkt) encoded)---testIssuerFingerprintRejectsUnknownVersion :: Assertion-testIssuerFingerprintRejectsUnknownVersion = do- let encoded =- runPut $ do- putWord8 34- putWord8 33- putWord8 5- putByteString (B.replicate 32 0)- case runGet (get :: Get SigSubPacket) encoded of- Left _ -> pure ()- Right _ -> assertFailure "issuer fingerprint subpacket version 5 should be rejected"--testV6Ed25519PublicKeySerializesFixedLength :: Assertion-testV6Ed25519PublicKeySerializesFixedLength = do- let pkp =- PKPayload- V6- (ThirtyTwoBitTimeStamp 0)- 0- EdDSA- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint 1)))- encoded = runPut (put (PublicKeyPkt pkp))- assertEqual- "v6 Ed25519 public key serialization uses fixed-length octet strings"- 44- (BL.length encoded)--testV4Ed25519PublicKeyParsesNativeFixedLength :: Assertion-testV4Ed25519PublicKeyParsesNativeFixedLength = do- let raw = B.pack [0x01 .. 0x20]- encoded =- runPut $ do- putWord8 0xc6- putWord8 38- putWord8 4- putWord32be 0- putWord8 (fromIntegral (fromFVal PKA.Ed25519))- putByteString raw- case runGet (get :: Get Pkt) encoded of- Right (PublicKeyPkt (PKPayload V4 (ThirtyTwoBitTimeStamp 0) _ PKA.Ed25519 (EdDSAPubKey Ed25519 (NativeEPoint (EPoint x))))) ->- assertEqual "v4 Ed25519 fixed-length key material parsed as native point" (os2ip raw) x- other ->- assertFailure- ("Expected v4 Ed25519 fixed-length public key parse, got " ++ show other)--testV4X25519PublicSubkeyParsesNativeFixedLength :: Assertion-testV4X25519PublicSubkeyParsesNativeFixedLength = do- let raw = B.pack [0x01 .. 0x20]- encoded =- runPut $ do- putWord8 0xce- putWord8 38- putWord8 4- putWord32be 0- putWord8 (fromIntegral (fromFVal PKA.X25519))- putByteString raw- case runGet (get :: Get Pkt) encoded of- Right (PublicSubkeyPkt (PKPayload V4 (ThirtyTwoBitTimeStamp 0) _ PKA.X25519 (EdDSAPubKey Ed25519 (NativeEPoint (EPoint x))))) ->- assertEqual "v4 X25519 fixed-length key material parsed as native point" (os2ip raw) x- other ->- assertFailure- ("Expected v4 X25519 fixed-length public subkey parse, got " ++ show other)--testV6SignatureIssuerFingerprintVersionMismatch :: Assertion-testV6SignatureIssuerFingerprintVersionMismatch = do- let signer =- PKPayload- V6- (ThirtyTwoBitTimeStamp 0)- 0- EdDSA- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint 1)))- sig =- SignaturePkt- (SigV6- BinarySig- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0))- [SigSubPacket False (IssuerFingerprint IssuerFingerprintV4 (fingerprint signer))]- []- 0- (NE.fromList [MPI 0, MPI 0]))- verifier _ _ _ = Right (Verification signer (case sig of SignaturePkt sp -> sp; _ -> error "impossible") [])- case verifySigWith verifier sig emptyPSC Nothing of- Left IssuerFingerprintSubpacketMismatch -> pure ()- Left err -> assertFailure ("unexpected verification error: " ++ show err)- Right _ -> assertFailure "v6 signature with issuer fingerprint version 4 should be rejected"--testPointAtInfinitySerialization :: Assertion-testPointAtInfinitySerialization =- assertEqual "point at infinity should not serialize" Nothing (point2MBS ECCT.PointO)--testPointSerializationRejectsMismatchedCoordinateWidths :: Assertion-testPointSerializationRejectsMismatchedCoordinateWidths =- assertEqual- "point serialization should reject mismatched coordinate widths"- Nothing- (point2MBS (ECCT.Point 1 256))--testSEIPDv2PacketRoundTrip :: Assertion-testSEIPDv2PacketRoundTrip = do- let salt = Salt (B.pack [0x00 .. 0x1f])- pkt = SymEncIntegrityProtectedDataPkt (SEIPD2 AES256 OCB 16 salt "\x01\x02\x03\x04")- encoded = runPut (put pkt)- assertEqual- "SEIPD v2 packet roundtrip"- (Right pkt)- (runGet (get :: Get Pkt) encoded)--testSEIPDv2RejectInvalidChunkSize :: Assertion-testSEIPDv2RejectInvalidChunkSize = do- let encoded =- runPut $ do- putWord8 0xd2- putWord8 37- putWord8 2- putWord8 7- putWord8 2- putWord8 17- putByteString (B.replicate 32 0)- putWord8 0- case runGet (get :: Get Pkt) encoded of- Right BrokenPacketPkt {} -> return ()- other ->- assertFailure- ("SEIPD v2 parser should reject chunk sizes larger than 16, got " ++- show other)--testPKESKv6ParsesAsV6WithoutLegacyFallback :: Assertion-testPKESKv6ParsesAsV6WithoutLegacyFallback = do- let recipientKeyIdentifier = BL.pack (0x04 : replicate 20 0)- esk = "\x00\x00"- encoded =- runPut $ do- putWord8 0xc1- putWord8 26- putWord8 6- putWord8 21- putByteString (BL.toStrict recipientKeyIdentifier)- putWord8 1- putByteString (BL.toStrict esk)- case runGet (get :: Get Pkt) encoded of- Right (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka parsedEsk))) -> do- assertEqual "PKESKv6 recipient key identifier" recipientKeyIdentifier rid- assertEqual "PKESKv6 algorithm" RSA pka- assertEqual "PKESKv6 ESK payload" esk parsedEsk- other ->- assertFailure ("Expected PKESKPkt (PKESK6 ...) parse result, got " ++ show other)--testPKESKv6RejectsInvalidRecipientIdentifierVersion :: Assertion-testPKESKv6RejectsInvalidRecipientIdentifierVersion = do- let recipientKeyIdentifier = BL.pack (0x05 : replicate 20 0)- encoded =- runPut $ do- putWord8 0xc1- putWord8 24- putWord8 6- putWord8 21- putByteString (BL.toStrict recipientKeyIdentifier)- putWord8 1- case runGet (get :: Get Pkt) encoded of- Right BrokenPacketPkt {} -> pure ()- other ->- assertFailure- ("Expected malformed PKESKv6 key version to produce BrokenPacketPkt, got " ++- show other)--testPKESKv6RejectsRecipientIdentifierLengthVersionMismatch :: Assertion-testPKESKv6RejectsRecipientIdentifierLengthVersionMismatch = do- let recipientKeyIdentifier = BL.pack (0x04 : replicate 32 0)- encoded =- runPut $ do- putWord8 0xc1- putWord8 36- putWord8 6- putWord8 33- putByteString (BL.toStrict recipientKeyIdentifier)- putWord8 1- case runGet (get :: Get Pkt) encoded of- Right BrokenPacketPkt {} -> pure ()- other ->- assertFailure- ("Expected malformed PKESKv6 recipient length/version mismatch to produce BrokenPacketPkt, got " ++- show other)--testLegacyPKESKRSARejectsExtraMPI :: Assertion-testLegacyPKESKRSARejectsExtraMPI = do- let encoded =- runPut $ do- putWord8 0xc1- putWord8 16- putWord8 3- putByteString (B.replicate 8 0)- putWord8 1- put (MPI 1)- put (MPI 2)- case runGet (get :: Get Pkt) encoded of- Right BrokenPacketPkt {} -> pure ()- other ->- assertFailure- ("Expected malformed legacy RSA PKESK to produce BrokenPacketPkt, got " ++- show other)--testLegacyPKESKECDHRejectsWrongMPICount :: Assertion-testLegacyPKESKECDHRejectsWrongMPICount = do- let encoded =- runPut $ do- putWord8 0xc1- putWord8 13- putWord8 3- putByteString (B.replicate 8 0)- putWord8 18- put (MPI 1)- case runGet (get :: Get Pkt) encoded of- Right BrokenPacketPkt {} -> pure ()- other ->- assertFailure- ("Expected malformed legacy ECDH PKESK to produce BrokenPacketPkt, got " ++- show other)--testLegacyPKESKX25519RejectsWrongMPICount :: Assertion-testLegacyPKESKX25519RejectsWrongMPICount = do- let encoded =- runPut $ do- putWord8 0xc1- putWord8 13- putWord8 3- putByteString (B.replicate 8 0)- putWord8 (fromIntegral (fromFVal X25519))- put (MPI 1)- case runGet (get :: Get Pkt) encoded of- Right BrokenPacketPkt {} -> pure ()- other ->- assertFailure- ("Expected malformed legacy X25519 PKESK to produce BrokenPacketPkt, got " ++- show other)--testLegacyUnencryptedSecretKeyRejectsChecksumMismatch :: Assertion-testLegacyUnencryptedSecretKeyRejectsChecksumMismatch = do- secretPackets <-- DC.runConduitRes $- CB.sourceFile "tests/data/unencrypted.seckey" DC..| conduitGet get DC..| CL.consume- pkt <-- case secretPackets of- (SecretKeyPkt pkp (SUUnencrypted sk checksum):_) ->- pure (SecretKeyPkt pkp (SUUnencrypted sk (checksum `xor` 1)))- (SecretKeyPkt _ _ :_) ->- assertFailure "unencrypted.seckey did not begin with an unencrypted secret key packet" >>- fail "expected unencrypted secret key packet"- _ ->- assertFailure "unencrypted.seckey did not begin with a secret key packet" >>- fail "expected secret key packet"- let encoded = runPut (put pkt)- case runGet (get :: Get Pkt) encoded of- Right BrokenPacketPkt {} -> pure ()- other ->- assertFailure- ("Expected unencrypted secret key checksum mismatch to produce BrokenPacketPkt, got " ++- show other)--testLegacySecretKeyRejectsUnsupportedSymmetricAlgorithmIVSizing :: Assertion-testLegacySecretKeyRejectsUnsupportedSymmetricAlgorithmIVSizing = do- let pkt =- SecretKeyPkt- (PKPayload V4 0 0 RSA (UnknownPKey BL.empty))- (SUSSHA1- (OtherSA 0xfe)- (IteratedSalted SHA256 (Salt8 "12345678") (IterationCount 65536))- (IV (B.replicate 8 0))- BL.empty)- encoded = runPut (put pkt)- case runGet (get :: Get Pkt) encoded of- Right BrokenPacketPkt {} -> pure ()- other ->- assertFailure- ("Expected legacy secret key with unsupported symmetric algorithm to produce BrokenPacketPkt, got " ++- show other)--testLegacyPKESKX25519ParsesRFC9580V3OctetLayout :: Assertion-testLegacyPKESKX25519ParsesRFC9580V3OctetLayout = do- let ephemeral = B.pack [0x01 .. 0x20]- wrappedWithAlgo = B.singleton (fromIntegral (fromFVal AES128)) <> B.replicate 24 0x5a- encoded =- runPut $ do- putWord8 0xc1- putWord8 (fromIntegral (1 + 8 + 1 + B.length ephemeral + 1 + B.length wrappedWithAlgo))- putWord8 3- putByteString (B.replicate 8 0)- putWord8 (fromIntegral (fromFVal X25519))- putByteString ephemeral- putWord8 (fromIntegral (B.length wrappedWithAlgo))- putByteString wrappedWithAlgo- case runGet (get :: Get Pkt) encoded of- Right (PKESKPkt (PKESKPayloadV3Packet (PKESKPayloadV3 3 _ X25519 (ephMPI :| [eskMPI])))) -> do- assertEqual "legacy X25519 v3 octet-layout ephemeral parse" (MPI (os2ip ephemeral)) ephMPI- assertEqual- "legacy X25519 v3 octet-layout wrapped parse"- (MPI (os2ip wrappedWithAlgo))- eskMPI- other ->- assertFailure- ("Expected legacy X25519 PKESK v3 octet-layout parse success, got " ++ show other)--testSigV6RejectsInvalidSaltSize :: Assertion-testSigV6RejectsInvalidSaltSize = do- let encoded =- runPut $ do- putWord8 0xc2- putWord8 45- putWord8 6- putWord8 0- putWord8 1- putWord8 8- putWord32be 0- putWord32be 0- putWord16be 0- putWord8 32- putByteString (B.replicate 32 0)- putWord16be 0- case runGet (get :: Get Pkt) encoded of- Right BrokenPacketPkt {} -> return ()- other ->- assertFailure- ("Expected invalid SigV6 salt size to produce BrokenPacketPkt, got " ++- show other)--testOPS6RejectsInvalidSaltSize :: Assertion-testOPS6RejectsInvalidSaltSize = do- let encoded =- runPut $ do- putWord8 0xc4- putWord8 69- putWord8 6- putWord8 0- putWord8 10- putWord8 22- putWord8 31- putByteString (B.replicate 31 0)- putByteString (B.replicate 32 0)- putWord8 0- case runGet (get :: Get Pkt) encoded of- Right BrokenPacketPkt {} -> return ()- other ->- assertFailure- ("Expected invalid OPS6 salt size to produce BrokenPacketPkt, got " ++- show other)--testOPS3RejectsInvalidNestedFlagOctet :: Assertion-testOPS3RejectsInvalidNestedFlagOctet = do- let encoded =- runPut $ do- putWord8 0xc4- putWord8 13- putWord8 3- putWord8 0- putWord8 8- putWord8 1- putByteString (B.replicate 8 0)- putWord8 2- case runGet (get :: Get Pkt) encoded of- Right BrokenPacketPkt {} -> pure ()- other ->- assertFailure- ("Expected invalid OPS3 nested-flag octet to produce BrokenPacketPkt, got " ++- show other)--testOPS6RejectsInvalidNestedFlagOctet :: Assertion-testOPS6RejectsInvalidNestedFlagOctet = do- let encoded =- runPut $ do- putWord8 0xc4- putWord8 70- putWord8 6- putWord8 0- putWord8 8- putWord8 1- putWord8 32- putByteString (B.replicate 32 0)- putByteString (B.replicate 32 0)- putWord8 2- case runGet (get :: Get Pkt) encoded of- Right BrokenPacketPkt {} -> pure ()- other ->- assertFailure- ("Expected invalid OPS6 nested-flag octet to produce BrokenPacketPkt, got " ++- show other)--mkECDHBoundaryTestPacket :: Pkt-mkECDHBoundaryTestPacket =- PublicKeyPkt- (PKPayload- V4- 0- 0- ECDH- (ECDHPubKey- (ECDSAPubKey- (ECDSA_PublicKey- (ECDSA.PublicKey (ECCT.getCurveByName ECCT.SEC_p256r1) (ECCT.Point 1 2))))- SHA256- AES128))--setStrictByteAt :: Int -> Word8 -> B.ByteString -> Maybe B.ByteString-setStrictByteAt idx w bs- | idx < 0 || idx >= B.length bs = Nothing- | otherwise =- let (prefix, rest) = B.splitAt idx bs- in case B.uncons rest of- Nothing -> Nothing- Just (_, suffix) -> Just (prefix <> B.singleton w <> suffix)--testECDHPubkeyRejectsReservedKDFLengthZero :: Assertion-testECDHPubkeyRejectsReservedKDFLengthZero = do- let encoded = BL.toStrict (runPut (put mkECDHBoundaryTestPacket))- mutated <-- maybe- (assertFailure "failed to locate ECDH KDF length byte for zero-length rejection test" >> fail "unreachable")- pure- (setStrictByteAt (B.length encoded - 4) 0x00 encoded)- case runGet (get :: Get Pkt) (BL.fromStrict mutated) of- Right BrokenPacketPkt {} -> pure ()- other ->- assertFailure- ("Expected ECDH KDF length 0 to produce BrokenPacketPkt, got " ++ show other)--testECDHPubkeyRejectsReservedKDFLength255 :: Assertion-testECDHPubkeyRejectsReservedKDFLength255 = do- let encoded = BL.toStrict (runPut (put mkECDHBoundaryTestPacket))- mutated <-- maybe- (assertFailure "failed to locate ECDH KDF length byte for 0xff rejection test" >> fail "unreachable")- pure- (setStrictByteAt (B.length encoded - 4) 0xff encoded)- case runGet (get :: Get Pkt) (BL.fromStrict mutated) of- Right BrokenPacketPkt {} -> pure ()- other ->- assertFailure- ("Expected ECDH KDF length 255 to produce BrokenPacketPkt, got " ++ show other)--testECDHPubkeyEncodesFixedKDFLength :: Assertion-testECDHPubkeyEncodesFixedKDFLength = do- let encoded = BL.toStrict (runPut (put mkECDHBoundaryTestPacket))- trailer = B.drop (B.length encoded - 4) encoded- assertEqual- "ECDH public-key encoding should emit fixed KDF trailer [3,1,hash,sym]"- (B.pack [0x03, 0x01, fromFVal SHA256, fromFVal AES128])- trailer--testKeyFlagsSubpacketEncodesEmptySetWithZeroOctet :: Assertion-testKeyFlagsSubpacketEncodesEmptySetWithZeroOctet = do- let encoded = runPut (put (SigSubPacket False (KeyFlags Set.empty)))- assertEqual- "empty key-flags subpacket should encode an explicit zero flags octet"- [2, 27, 0]- (BL.unpack encoded)- case runGet (get :: Get SigSubPacket) encoded of- Right (SigSubPacket False (KeyFlags flags)) ->- assertEqual- "empty key-flags subpacket should decode back to an empty flag set"- Set.empty- flags- other ->- assertFailure- ("Expected empty key-flags subpacket roundtrip, got " ++ show other)--expectedV6SaltSizeForTest :: HashAlgorithm -> Maybe Int-expectedV6SaltSizeForTest =- fmap fromIntegral . signatureV6SaltSizeForHashAlgorithm--signatureHasIssuerFingerprintV6 :: Fingerprint -> SignaturePayload -> Bool-signatureHasIssuerFingerprintV6 expectedFp (SigV6 _ _ _ _ hashed unhashed _ _) =- expectedFp `elem`- [ ifp- | SigSubPacket _ (IssuerFingerprint IssuerFingerprintV6 ifp) <- hashed ++ unhashed- ]-signatureHasIssuerFingerprintV6 _ _ = False--testV6SecretFixtureSignatureSemantics :: Assertion-testV6SecretFixtureSignatureSemantics = do- armors <- loadArmor "v6-secret.pgp.aa"- payload <-- case armors of- (a:_) -> pure (armorPayload a)- [] ->- assertFailure "v6-secret.pgp.aa should contain one armored payload" >>- fail "expected one armored payload"- let packets = parsePkts payload- primaryV6Key =- listToMaybe- [ pkp- | SecretKeyPkt pkp _ <- packets- , _keyVersion pkp == V6- ] <|>- listToMaybe- [ pkp- | PublicKeyPkt pkp <- packets- , _keyVersion pkp == V6- ]- signatures =- [ (sig, ha, salt)- | SignaturePkt sig@(SigV6 _ _ ha salt _ _ _ _) <- packets- ]- pkp <-- case primaryV6Key of- Nothing ->- assertFailure "v6-secret.pgp.aa should contain a primary v6 key packet" >>- fail "expected primary v6 key packet"- Just k -> pure k- assertBool- "v6-secret.pgp.aa should contain at least one SigV6 packet"- (not (null signatures))- mapM_- (\(_, ha, salt) ->- case expectedV6SaltSizeForTest ha of- Nothing ->- assertFailure ("SigV6 in v6-secret.pgp.aa uses unsupported salt hash algorithm: " ++ show ha)- Just expected ->- assertEqual- "SigV6 salt size in v6-secret.pgp.aa should match hash algorithm"- expected- (fromIntegral (BL.length (unSignatureSalt salt))))- signatures- assertBool- "v6-secret.pgp.aa should include at least one IssuerFingerprint v6 matching the primary key"- (any (\(sig, _, _) -> signatureHasIssuerFingerprintV6 (fingerprint pkp) sig) signatures)--testV6SecretFixtureDerivesEightOctetKeyID :: Assertion-testV6SecretFixtureDerivesEightOctetKeyID = do- armors <- loadArmor "v6-secret.pgp.aa"- payload <-- case armors of- (a:_) -> pure (armorPayload a)- [] ->- assertFailure "v6-secret.pgp.aa should contain one armored payload" >>- fail "expected one armored payload"- let packets = parsePkts payload- primaryV6Key =- listToMaybe- [ pkp- | SecretKeyPkt pkp _ <- packets- , _keyVersion pkp == V6- ] <|>- listToMaybe- [ pkp- | PublicKeyPkt pkp <- packets- , _keyVersion pkp == V6- ]- pkp <-- case primaryV6Key of- Nothing ->- assertFailure "v6-secret.pgp.aa should contain a primary v6 key packet" >>- fail "expected primary v6 key packet"- Just k -> pure k- derivedKeyId <-- case eightOctetKeyID pkp of- Left err ->- assertFailure ("Expected v6 eight-octet key-id derivation to succeed: " ++ err) >>- fail "expected v6 eight-octet key-id"- Right keyId -> pure keyId- let expectedKeyId = EightOctetKeyId (BL.take 8 (unFingerprint (fingerprint pkp)))- assertEqual- "v6 eight-octet key-id should be the high-order 64 bits of the fingerprint"- expectedKeyId- derivedKeyId+{-# LANGUAGE OverloadedStrings #-}++module Tests.Serialization (serializationTests) where++import Control.Applicative ((<|>))+import Control.Monad (forM_)+import Crypto.Number.Serialize (os2ip)+import qualified Crypto.PubKey.ECC.ECDSA as ECDSA+import qualified Crypto.PubKey.ECC.Types as ECCT+import Data.Binary (Get, get, put)+import Data.Binary.Put+ ( putByteString+ , putWord16be+ , putWord32be+ , putWord8+ , runPut+ )+import Data.Bits (xor)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import qualified Data.Conduit as DC+import qualified Data.Conduit.Binary as CB+import qualified Data.Conduit.List as CL+import Data.Conduit.Serialization.Binary (conduitGet)+import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NE+import Data.Maybe (listToMaybe)+import qualified Data.Set as Set+import Data.Word (Word8)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit+ ( Assertion+ , assertBool+ , assertEqual+ , assertFailure+ , testCase+ )++import Codec.Encryption.OpenPGP.Fingerprint+ ( eightOctetKeyID+ , fingerprint+ )+import Codec.Encryption.OpenPGP.Internal (emptyPSC, point2MBS)+import Codec.Encryption.OpenPGP.KeyringParser+ ( parseTKsWithWireRep+ )+import Codec.Encryption.OpenPGP.Policy+ ( defaultVerificationPolicy+ , signatureV6SaltSizeForHashAlgorithm+ )+import Codec.Encryption.OpenPGP.Serialize+ ( parsePkts+ , parsePktsWithWireRep+ )+import Codec.Encryption.OpenPGP.Signatures+ ( VerificationError (..)+ , verifySigWith+ )+import Codec.Encryption.OpenPGP.Types+import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as PKA+import Tests.Common+ ( armorPayload+ , loadArmor+ , readFixturePayload+ , runGet+ )++serializationTests :: TestTree+serializationTests =+ testGroup+ "Serialization"+ [ testGroup+ "Serialization group"+ [ testCase+ "000001-006.public_key"+ (testSerialization "000001-006.public_key")+ , testCase+ "issuer-fingerprint-rejects-unknown-version"+ testIssuerFingerprintRejectsUnknownVersion+ , testCase+ "v6-ed25519-public-key-serializes-fixed-length"+ testV6Ed25519PublicKeySerializesFixedLength+ , testCase+ "v4-ed25519-public-key-parses-native-fixed-length"+ testV4Ed25519PublicKeyParsesNativeFixedLength+ , testCase+ "v4-x25519-public-subkey-parses-native-fixed-length"+ testV4X25519PublicSubkeyParsesNativeFixedLength+ , testCase+ "v6-signature-issuer-fingerprint-version-mismatch"+ testV6SignatureIssuerFingerprintVersionMismatch+ , testCase+ "000002-013.user_id"+ (testSerialization "000002-013.user_id")+ , testCase "000003-002.sig" (testSerialization "000003-002.sig")+ , testCase+ "000004-012.ring_trust"+ (testSerialization "000004-012.ring_trust")+ , testCase "000005-002.sig" (testSerialization "000005-002.sig")+ , testCase+ "000006-012.ring_trust"+ (testSerialization "000006-012.ring_trust")+ , testCase "000007-002.sig" (testSerialization "000007-002.sig")+ , testCase+ "000008-012.ring_trust"+ (testSerialization "000008-012.ring_trust")+ , testCase "000009-002.sig" (testSerialization "000009-002.sig")+ , testCase+ "000010-012.ring_trust"+ (testSerialization "000010-012.ring_trust")+ , testCase "000011-002.sig" (testSerialization "000011-002.sig")+ , testCase+ "000012-012.ring_trust"+ (testSerialization "000012-012.ring_trust")+ , testCase+ "000013-014.public_subkey"+ (testSerialization "000013-014.public_subkey")+ , testCase "000014-002.sig" (testSerialization "000014-002.sig")+ , testCase+ "000015-012.ring_trust"+ (testSerialization "000015-012.ring_trust")+ , testCase+ "000016-006.public_key"+ (testSerialization "000016-006.public_key")+ , testCase "000017-002.sig" (testSerialization "000017-002.sig")+ , testCase+ "000018-012.ring_trust"+ (testSerialization "000018-012.ring_trust")+ , testCase+ "000019-013.user_id"+ (testSerialization "000019-013.user_id")+ , testCase "000020-002.sig" (testSerialization "000020-002.sig")+ , testCase+ "000021-012.ring_trust"+ (testSerialization "000021-012.ring_trust")+ , testCase "000022-002.sig" (testSerialization "000022-002.sig")+ , testCase+ "000023-012.ring_trust"+ (testSerialization "000023-012.ring_trust")+ , testCase+ "000024-014.public_subkey"+ (testSerialization "000024-014.public_subkey")+ , testCase "000025-002.sig" (testSerialization "000025-002.sig")+ , testCase+ "000026-012.ring_trust"+ (testSerialization "000026-012.ring_trust")+ , testCase+ "000027-006.public_key"+ (testSerialization "000027-006.public_key")+ , testCase "000028-002.sig" (testSerialization "000028-002.sig")+ , testCase+ "000029-012.ring_trust"+ (testSerialization "000029-012.ring_trust")+ , testCase+ "000030-013.user_id"+ (testSerialization "000030-013.user_id")+ , testCase "000031-002.sig" (testSerialization "000031-002.sig")+ , testCase+ "000032-012.ring_trust"+ (testSerialization "000032-012.ring_trust")+ , testCase "000033-002.sig" (testSerialization "000033-002.sig")+ , testCase+ "000034-012.ring_trust"+ (testSerialization "000034-012.ring_trust")+ , testCase+ "000035-006.public_key"+ (testSerialization "000035-006.public_key")+ , testCase+ "000036-013.user_id"+ (testSerialization "000036-013.user_id")+ , testCase "000037-002.sig" (testSerialization "000037-002.sig")+ , testCase+ "000038-012.ring_trust"+ (testSerialization "000038-012.ring_trust")+ , testCase "000039-002.sig" (testSerialization "000039-002.sig")+ , testCase+ "000040-012.ring_trust"+ (testSerialization "000040-012.ring_trust")+ , testCase+ "000041-017.attribute"+ (testSerialization "000041-017.attribute")+ , testCase "000042-002.sig" (testSerialization "000042-002.sig")+ , testCase+ "000043-012.ring_trust"+ (testSerialization "000043-012.ring_trust")+ , testCase+ "000044-014.public_subkey"+ (testSerialization "000044-014.public_subkey")+ , testCase "000045-002.sig" (testSerialization "000045-002.sig")+ , testCase+ "000046-012.ring_trust"+ (testSerialization "000046-012.ring_trust")+ , testCase+ "000047-005.secret_key"+ (testSerialization "000047-005.secret_key")+ , testCase+ "000048-013.user_id"+ (testSerialization "000048-013.user_id")+ , testCase "000049-002.sig" (testSerialization "000049-002.sig")+ , testCase+ "000050-012.ring_trust"+ (testSerialization "000050-012.ring_trust")+ , testCase+ "000051-007.secret_subkey"+ (testSerialization "000051-007.secret_subkey")+ , testCase "000052-002.sig" (testSerialization "000052-002.sig")+ , testCase+ "000053-012.ring_trust"+ (testSerialization "000053-012.ring_trust")+ , testCase+ "000054-005.secret_key"+ (testSerialization "000054-005.secret_key")+ , testCase "000055-002.sig" (testSerialization "000055-002.sig")+ , testCase+ "000056-012.ring_trust"+ (testSerialization "000056-012.ring_trust")+ , testCase+ "000057-013.user_id"+ (testSerialization "000057-013.user_id")+ , testCase "000058-002.sig" (testSerialization "000058-002.sig")+ , testCase+ "000059-012.ring_trust"+ (testSerialization "000059-012.ring_trust")+ , testCase+ "000060-007.secret_subkey"+ (testSerialization "000060-007.secret_subkey")+ , testCase "000061-002.sig" (testSerialization "000061-002.sig")+ , testCase+ "000062-012.ring_trust"+ (testSerialization "000062-012.ring_trust")+ , testCase+ "000063-005.secret_key"+ (testSerialization "000063-005.secret_key")+ , testCase "000064-002.sig" (testSerialization "000064-002.sig")+ , testCase+ "000065-012.ring_trust"+ (testSerialization "000065-012.ring_trust")+ , testCase+ "000066-013.user_id"+ (testSerialization "000066-013.user_id")+ , testCase "000067-002.sig" (testSerialization "000067-002.sig")+ , testCase+ "000068-012.ring_trust"+ (testSerialization "000068-012.ring_trust")+ , testCase+ "000069-005.secret_key"+ (testSerialization "000069-005.secret_key")+ , testCase+ "000070-013.user_id"+ (testSerialization "000070-013.user_id")+ , testCase "000071-002.sig" (testSerialization "000071-002.sig")+ , testCase+ "000072-012.ring_trust"+ (testSerialization "000072-012.ring_trust")+ , testCase+ "000073-017.attribute"+ (testSerialization "000073-017.attribute")+ , testCase "000074-002.sig" (testSerialization "000074-002.sig")+ , testCase+ "000075-012.ring_trust"+ (testSerialization "000075-012.ring_trust")+ , testCase+ "000076-007.secret_subkey"+ (testSerialization "000076-007.secret_subkey")+ , testCase "000077-002.sig" (testSerialization "000077-002.sig")+ , testCase+ "000078-012.ring_trust"+ (testSerialization "000078-012.ring_trust")+ , testCase "pubring.gpg" (testSerialization "pubring.gpg")+ , testCase "secring.gpg" (testSerialization "secring.gpg")+ , testCase+ "compressedsig.gpg"+ (testSerialization "compressedsig.gpg")+ , testCase+ "compressedsig-zlib.gpg"+ (testSerialization "compressedsig-zlib.gpg")+ , testCase+ "compressedsig-bzip2.gpg"+ (testSerialization "compressedsig-bzip2.gpg")+ , testCase "onepass_sig" (testSerialization "onepass_sig")+ , testCase+ "uncompressed-ops-dsa.gpg"+ (testSerialization "uncompressed-ops-dsa.gpg")+ , testCase+ "uncompressed-ops-rsa.gpg"+ (testSerialization "uncompressed-ops-rsa.gpg")+ , testCase "simple.seckey" (testSerialization "simple.seckey")+ , testCase+ "v3-genericcert.sig"+ (testSerialization "v3-genericcert.sig")+ , testCase+ "sigs-with-regexes"+ (testSerialization "sigs-with-regexes")+ , testCase+ "gnu-dummy-s2k-101-secret-key.gpg"+ (testSerialization "gnu-dummy-s2k-101-secret-key.gpg")+ , testCase+ "anibal-ed25519.gpg"+ (testSerialization "anibal-ed25519.gpg")+ , testCase+ "nist_p-256_key.gpg"+ (testSerialization "nist_p-256_key.gpg")+ , testCase+ "nist_p-256_secretkey.gpg"+ (testSerialization "nist_p-256_secretkey.gpg")+ , testCase+ "v6-secret.pgp.aa"+ (testSerialization "v6-secret.pgp.aa")+ , testCase+ "sample-eddsa.pubkey"+ (testSerialization "sample-eddsa.pubkey")+ , testCase+ "should not serialize point at infinity"+ testPointAtInfinitySerialization+ , testCase+ "should reject mismatched EC coordinate widths"+ testPointSerializationRejectsMismatchedCoordinateWidths+ ]+ , testGroup+ "TKUnknown Serialization group"+ [ testCase+ "pubring.gpg TKUnknown serialization"+ (testTKSerialization "pubring.gpg")+ , testCase+ "secring.gpg TKUnknown serialization"+ (testTKSerialization "secring.gpg")+ ]+ , testGroup+ "Argon2 S2K group"+ [ testCase+ "Argon2 SKESK packet roundtrip"+ testArgon2S2KPacketRoundTrip+ ]+ , testGroup+ "RFC9580 SEIPD v2 group"+ [ testCase "SEIPD v2 packet roundtrip" testSEIPDv2PacketRoundTrip+ , testCase+ "SEIPD v2 rejects invalid chunk size"+ testSEIPDv2RejectInvalidChunkSize+ , testCase+ "PKESKv6 parsing does not fall back to legacy parser"+ testPKESKv6ParsesAsV6WithoutLegacyFallback+ , testCase+ "PKESKv6 rejects invalid recipient key version"+ testPKESKv6RejectsInvalidRecipientIdentifierVersion+ , testCase+ "PKESKv6 rejects recipient length/version mismatches"+ testPKESKv6RejectsRecipientIdentifierLengthVersionMismatch+ , testCase+ "legacy RSA PKESK rejects extra MPIs"+ testLegacyPKESKRSARejectsExtraMPI+ , testCase+ "legacy ECDH PKESK rejects wrong MPI count"+ testLegacyPKESKECDHRejectsWrongMPICount+ , testCase+ "legacy X25519 PKESK rejects wrong MPI count"+ testLegacyPKESKX25519RejectsWrongMPICount+ , testCase+ "legacy unencrypted secret key rejects checksum mismatch"+ testLegacyUnencryptedSecretKeyRejectsChecksumMismatch+ , testCase+ "legacy secret key rejects unsupported symmetric algorithm IV sizing"+ testLegacySecretKeyRejectsUnsupportedSymmetricAlgorithmIVSizing+ , testCase+ "legacy X25519 PKESK parses RFC9580 v3 octet layout"+ testLegacyPKESKX25519ParsesRFC9580V3OctetLayout+ , testCase+ "SigV6 rejects invalid salt size"+ testSigV6RejectsInvalidSaltSize+ , testCase+ "OPS6 rejects invalid salt size"+ testOPS6RejectsInvalidSaltSize+ , testCase+ "OPS3 rejects invalid nested-flag octet"+ testOPS3RejectsInvalidNestedFlagOctet+ , testCase+ "OPS6 rejects invalid nested-flag octet"+ testOPS6RejectsInvalidNestedFlagOctet+ , testCase+ "ECDH pubkey rejects reserved KDF length 0"+ testECDHPubkeyRejectsReservedKDFLengthZero+ , testCase+ "ECDH pubkey rejects reserved KDF length 255"+ testECDHPubkeyRejectsReservedKDFLength255+ , testCase+ "ECDH pubkey encodes fixed KDF length trailer"+ testECDHPubkeyEncodesFixedKDFLength+ , testCase+ "empty key-flags subpacket encodes explicit zero octet"+ testKeyFlagsSubpacketEncodesEmptySetWithZeroOctet+ , testCase+ "v6-secret fixture SigV6 semantics"+ testV6SecretFixtureSignatureSemantics+ , testCase+ "v6-secret fixture derives eight-octet key-id from fingerprint prefix"+ testV6SecretFixtureDerivesEightOctetKeyID+ ]+ ]++testSerialization :: FilePath -> Assertion+testSerialization fpr = do+ bs <- readFixturePayload fpr+ let firstpass = runGet get bs+ case fmap unBlock firstpass of+ Left _ -> assertFailure $ "First pass failed on " ++ fpr+ Right [] ->+ assertFailure $ "First pass of " ++ fpr ++ " decoded to nothing."+ Right packs -> do+ let roundtrip = runPut $ put (Block packs)+ let secondpass = runGet (get :: Get (Block Pkt)) roundtrip+ if fmap unBlock secondpass == Right []+ then+ assertFailure $+ "Second pass of " ++ fpr ++ " decoded to nothing."+ else assertEqual ("for " ++ fpr) firstpass secondpass++testTKSerialization :: FilePath -> Assertion+testTKSerialization fpr = do+ bs <- readFixturePayload fpr+ let pkts = parsePktsWithWireRep (wireRepRef bs) bs+ tksWithWireRep = parseTKsWithWireRep True pkts+ if null tksWithWireRep+ then+ assertFailure $+ "TKUnknown serialization test: " ++ fpr ++ " parsed to no TKs"+ else forM_ tksWithWireRep (testTKRoundtrip fpr)++testTKRoundtrip :: FilePath -> TKWithWireRep -> Assertion+testTKRoundtrip fpr tk = do+ let packets = _tkPackets tk+ encoded = runPut (put (Block (map _pktValue packets)))+ case runGet (get :: Get (Block Pkt)) encoded of+ Left err ->+ assertFailure $+ "TKUnknown " ++ fpr ++ " packet re-parse failed: " ++ err+ Right reparsedBlock ->+ assertEqual+ ("TKUnknown packet re-serialization roundtrip for " ++ fpr)+ (Block (map _pktValue packets))+ reparsedBlock+ case toStructuredTKWithWireRep tk of+ Left err ->+ assertFailure $+ "TKUnknown structured conversion failed for "+ ++ fpr+ ++ ": "+ ++ show err+ Right structured ->+ case canonicalizeTKStructuredWithWireRep structured of+ Left err ->+ assertFailure $+ "TKUnknown canonical conversion failed for "+ ++ fpr+ ++ ": "+ ++ show err+ Right _canonical -> pure ()++testArgon2S2KPacketRoundTrip :: Assertion+testArgon2S2KPacketRoundTrip = do+ let s2k = Argon2 (Salt16 (B.pack [0x00 .. 0x0f])) 1 4 15+ pkt =+ SKESKPkt+ (SKESKPayloadV4Packet (SKESKPayloadV4 AES128 s2k Nothing))+ encoded = runPut (put pkt)+ assertEqual+ "Argon2 S2K SKESK packet roundtrip"+ (Right pkt)+ (runGet (get :: Get Pkt) encoded)++testIssuerFingerprintRejectsUnknownVersion :: Assertion+testIssuerFingerprintRejectsUnknownVersion = do+ let encoded =+ runPut $ do+ putWord8 34+ putWord8 33+ putWord8 5+ putByteString (B.replicate 32 0)+ case runGet (get :: Get SigSubPacket) encoded of+ Left _ -> pure ()+ Right _ ->+ assertFailure+ "issuer fingerprint subpacket version 5 should be rejected"++testV6Ed25519PublicKeySerializesFixedLength :: Assertion+testV6Ed25519PublicKeySerializesFixedLength = do+ let pkp =+ PKPayload+ V6+ (ThirtyTwoBitTimeStamp 0)+ 0+ EdDSA+ (EdDSAPubKey EdSigningCurve25519 (NativeEPoint (EPoint 1)))+ encoded = runPut (put (PublicKeyPkt pkp))+ assertEqual+ "v6 Ed25519 public key serialization uses fixed-length octet strings"+ 44+ (BL.length encoded)++testV4Ed25519PublicKeyParsesNativeFixedLength :: Assertion+testV4Ed25519PublicKeyParsesNativeFixedLength = do+ let raw = B.pack [0x01 .. 0x20]+ encoded =+ runPut $ do+ putWord8 0xc6+ putWord8 38+ putWord8 4+ putWord32be 0+ putWord8 (fromIntegral (fromFVal PKA.Ed25519))+ putByteString raw+ case runGet (get :: Get Pkt) encoded of+ Right+ ( PublicKeyPkt+ ( PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ _+ PKA.Ed25519+ (EdDSAPubKey EdSigningCurve25519 (NativeEPoint (EPoint x)))+ )+ ) ->+ assertEqual+ "v4 Ed25519 fixed-length key material parsed as native point"+ (os2ip raw)+ x+ other ->+ assertFailure+ ( "Expected v4 Ed25519 fixed-length public key parse, got "+ ++ show other+ )++testV4X25519PublicSubkeyParsesNativeFixedLength :: Assertion+testV4X25519PublicSubkeyParsesNativeFixedLength = do+ let raw = B.pack [0x01 .. 0x20]+ encoded =+ runPut $ do+ putWord8 0xce+ putWord8 38+ putWord8 4+ putWord32be 0+ putWord8 (fromIntegral (fromFVal PKA.X25519))+ putByteString raw+ case runGet (get :: Get Pkt) encoded of+ Right+ ( PublicSubkeyPkt+ ( PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ _+ PKA.X25519+ (EdDSAPubKey EdSigningCurve25519 (NativeEPoint (EPoint x)))+ )+ ) ->+ assertEqual+ "v4 X25519 fixed-length key material parsed as native point"+ (os2ip raw)+ x+ other ->+ assertFailure+ ( "Expected v4 X25519 fixed-length public subkey parse, got "+ ++ show other+ )++testV6SignatureIssuerFingerprintVersionMismatch :: Assertion+testV6SignatureIssuerFingerprintVersionMismatch = do+ let signer =+ PKPayload+ V6+ (ThirtyTwoBitTimeStamp 0)+ 0+ EdDSA+ (EdDSAPubKey EdSigningCurve25519 (NativeEPoint (EPoint 1)))+ sig =+ SignaturePkt+ ( SigV6+ BinarySig+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0))+ [ SigSubPacket+ False+ (IssuerFingerprint IssuerFingerprintV4 (fingerprint signer))+ ]+ []+ 0+ (NE.fromList [MPI 0, MPI 0])+ )+ verifier _ _ _ =+ Right+ ( Verification+ signer+ (case sig of SignaturePkt sp -> sp; _ -> error "impossible")+ []+ )+ case verifySigWith+ defaultVerificationPolicy+ verifier+ sig+ emptyPSC+ Nothing of+ Left IssuerFingerprintSubpacketMismatch -> pure ()+ Left err -> assertFailure ("unexpected verification error: " ++ show err)+ Right _ ->+ assertFailure+ "v6 signature with issuer fingerprint version 4 should be rejected"++testPointAtInfinitySerialization :: Assertion+testPointAtInfinitySerialization =+ assertEqual+ "point at infinity should not serialize"+ Nothing+ (point2MBS ECCT.PointO)++testPointSerializationRejectsMismatchedCoordinateWidths+ :: Assertion+testPointSerializationRejectsMismatchedCoordinateWidths =+ assertEqual+ "point serialization should reject mismatched coordinate widths"+ Nothing+ (point2MBS (ECCT.Point 1 256))++testSEIPDv2PacketRoundTrip :: Assertion+testSEIPDv2PacketRoundTrip = do+ let salt = Salt (B.pack [0x00 .. 0x1f])+ pkt =+ SymEncIntegrityProtectedDataPkt+ (SEIPD2 AES256 OCB 16 salt "\x01\x02\x03\x04")+ encoded = runPut (put pkt)+ assertEqual+ "SEIPD v2 packet roundtrip"+ (Right pkt)+ (runGet (get :: Get Pkt) encoded)++testSEIPDv2RejectInvalidChunkSize :: Assertion+testSEIPDv2RejectInvalidChunkSize = do+ let encoded =+ runPut $ do+ putWord8 0xd2+ putWord8 37+ putWord8 2+ putWord8 7+ putWord8 2+ putWord8 17+ putByteString (B.replicate 32 0)+ putWord8 0+ case runGet (get :: Get Pkt) encoded of+ Right BrokenPacketPkt {} -> return ()+ other ->+ assertFailure+ ( "SEIPD v2 parser should reject chunk sizes larger than 16, got "+ ++ show other+ )++testPKESKv6ParsesAsV6WithoutLegacyFallback :: Assertion+testPKESKv6ParsesAsV6WithoutLegacyFallback = do+ let recipientKeyIdentifier = BL.pack (0x04 : replicate 20 0)+ esk = "\x00\x00"+ encoded =+ runPut $ do+ putWord8 0xc1+ putWord8 26+ putWord8 6+ putWord8 21+ putByteString (BL.toStrict recipientKeyIdentifier)+ putWord8 1+ putByteString (BL.toStrict esk)+ case runGet (get :: Get Pkt) encoded of+ Right+ ( PKESKPkt+ (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka parsedEsk))+ ) -> do+ assertEqual+ "PKESKv6 recipient key identifier"+ recipientKeyIdentifier+ rid+ assertEqual "PKESKv6 algorithm" RSA pka+ assertEqual "PKESKv6 ESK payload" esk parsedEsk+ other ->+ assertFailure+ ("Expected PKESKPkt (PKESK6 ...) parse result, got " ++ show other)++testPKESKv6RejectsInvalidRecipientIdentifierVersion :: Assertion+testPKESKv6RejectsInvalidRecipientIdentifierVersion = do+ let recipientKeyIdentifier = BL.pack (0x05 : replicate 20 0)+ encoded =+ runPut $ do+ putWord8 0xc1+ putWord8 24+ putWord8 6+ putWord8 21+ putByteString (BL.toStrict recipientKeyIdentifier)+ putWord8 1+ case runGet (get :: Get Pkt) encoded of+ Right BrokenPacketPkt {} -> pure ()+ other ->+ assertFailure+ ( "Expected malformed PKESKv6 key version to produce BrokenPacketPkt, got "+ ++ show other+ )++testPKESKv6RejectsRecipientIdentifierLengthVersionMismatch+ :: Assertion+testPKESKv6RejectsRecipientIdentifierLengthVersionMismatch = do+ let recipientKeyIdentifier = BL.pack (0x04 : replicate 32 0)+ encoded =+ runPut $ do+ putWord8 0xc1+ putWord8 36+ putWord8 6+ putWord8 33+ putByteString (BL.toStrict recipientKeyIdentifier)+ putWord8 1+ case runGet (get :: Get Pkt) encoded of+ Right BrokenPacketPkt {} -> pure ()+ other ->+ assertFailure+ ( "Expected malformed PKESKv6 recipient length/version mismatch to produce BrokenPacketPkt, got "+ ++ show other+ )++testLegacyPKESKRSARejectsExtraMPI :: Assertion+testLegacyPKESKRSARejectsExtraMPI = do+ let encoded =+ runPut $ do+ putWord8 0xc1+ putWord8 16+ putWord8 3+ putByteString (B.replicate 8 0)+ putWord8 1+ put (MPI 1)+ put (MPI 2)+ case runGet (get :: Get Pkt) encoded of+ Right BrokenPacketPkt {} -> pure ()+ other ->+ assertFailure+ ( "Expected malformed legacy RSA PKESK to produce BrokenPacketPkt, got "+ ++ show other+ )++testLegacyPKESKECDHRejectsWrongMPICount :: Assertion+testLegacyPKESKECDHRejectsWrongMPICount = do+ let encoded =+ runPut $ do+ putWord8 0xc1+ putWord8 13+ putWord8 3+ putByteString (B.replicate 8 0)+ putWord8 18+ put (MPI 1)+ case runGet (get :: Get Pkt) encoded of+ Right BrokenPacketPkt {} -> pure ()+ other ->+ assertFailure+ ( "Expected malformed legacy ECDH PKESK to produce BrokenPacketPkt, got "+ ++ show other+ )++testLegacyPKESKX25519RejectsWrongMPICount :: Assertion+testLegacyPKESKX25519RejectsWrongMPICount = do+ let encoded =+ runPut $ do+ putWord8 0xc1+ putWord8 13+ putWord8 3+ putByteString (B.replicate 8 0)+ putWord8 (fromIntegral (fromFVal X25519))+ put (MPI 1)+ case runGet (get :: Get Pkt) encoded of+ Right BrokenPacketPkt {} -> pure ()+ other ->+ assertFailure+ ( "Expected malformed legacy X25519 PKESK to produce BrokenPacketPkt, got "+ ++ show other+ )++testLegacyUnencryptedSecretKeyRejectsChecksumMismatch+ :: Assertion+testLegacyUnencryptedSecretKeyRejectsChecksumMismatch = do+ secretPackets <-+ DC.runConduitRes $+ CB.sourceFile "tests/data/unencrypted.seckey"+ DC..| conduitGet get+ DC..| CL.consume+ pkt <-+ case secretPackets of+ (SecretKeyPkt pkp (SUUnencrypted sk checksum) : _) ->+ pure (SecretKeyPkt pkp (SUUnencrypted sk (checksum `xor` 1)))+ (SecretKeyPkt _ _ : _) ->+ assertFailure+ "unencrypted.seckey did not begin with an unencrypted secret key packet"+ >> fail "expected unencrypted secret key packet"+ _ ->+ assertFailure+ "unencrypted.seckey did not begin with a secret key packet"+ >> fail "expected secret key packet"+ let encoded = runPut (put pkt)+ case runGet (get :: Get Pkt) encoded of+ Right BrokenPacketPkt {} -> pure ()+ other ->+ assertFailure+ ( "Expected unencrypted secret key checksum mismatch to produce BrokenPacketPkt, got "+ ++ show other+ )++testLegacySecretKeyRejectsUnsupportedSymmetricAlgorithmIVSizing+ :: Assertion+testLegacySecretKeyRejectsUnsupportedSymmetricAlgorithmIVSizing = do+ let pkt =+ SecretKeyPkt+ (PKPayload V4 0 0 RSA (UnknownPKey BL.empty))+ ( SUSSHA1+ (OtherSA 0xfe)+ (IteratedSalted SHA256 (Salt8 "12345678") (IterationCount 65536))+ (IV (B.replicate 8 0))+ BL.empty+ )+ encoded = runPut (put pkt)+ case runGet (get :: Get Pkt) encoded of+ Right BrokenPacketPkt {} -> pure ()+ other ->+ assertFailure+ ( "Expected legacy secret key with unsupported symmetric algorithm to produce BrokenPacketPkt, got "+ ++ show other+ )++testLegacyPKESKX25519ParsesRFC9580V3OctetLayout :: Assertion+testLegacyPKESKX25519ParsesRFC9580V3OctetLayout = do+ let ephemeral = B.pack [0x01 .. 0x20]+ wrappedWithAlgo =+ B.singleton (fromIntegral (fromFVal AES128))+ <> B.replicate 24 0x5a+ encoded =+ runPut $ do+ putWord8 0xc1+ putWord8+ ( fromIntegral+ (1 + 8 + 1 + B.length ephemeral + 1 + B.length wrappedWithAlgo)+ )+ putWord8 3+ putByteString (B.replicate 8 0)+ putWord8 (fromIntegral (fromFVal X25519))+ putByteString ephemeral+ putWord8 (fromIntegral (B.length wrappedWithAlgo))+ putByteString wrappedWithAlgo+ case runGet (get :: Get Pkt) encoded of+ Right+ ( PKESKPkt+ ( PKESKPayloadV3Packet+ (PKESKPayloadV3 3 _ X25519 (ephMPI :| [eskMPI]))+ )+ ) -> do+ assertEqual+ "legacy X25519 v3 octet-layout ephemeral parse"+ (MPI (os2ip ephemeral))+ ephMPI+ assertEqual+ "legacy X25519 v3 octet-layout wrapped parse"+ (MPI (os2ip wrappedWithAlgo))+ eskMPI+ other ->+ assertFailure+ ( "Expected legacy X25519 PKESK v3 octet-layout parse success, got "+ ++ show other+ )++testSigV6RejectsInvalidSaltSize :: Assertion+testSigV6RejectsInvalidSaltSize = do+ let encoded =+ runPut $ do+ putWord8 0xc2+ putWord8 45+ putWord8 6+ putWord8 0+ putWord8 1+ putWord8 8+ putWord32be 0+ putWord32be 0+ putWord16be 0+ putWord8 32+ putByteString (B.replicate 32 0)+ putWord16be 0+ case runGet (get :: Get Pkt) encoded of+ Right BrokenPacketPkt {} -> return ()+ other ->+ assertFailure+ ( "Expected invalid SigV6 salt size to produce BrokenPacketPkt, got "+ ++ show other+ )++testOPS6RejectsInvalidSaltSize :: Assertion+testOPS6RejectsInvalidSaltSize = do+ let encoded =+ runPut $ do+ putWord8 0xc4+ putWord8 69+ putWord8 6+ putWord8 0+ putWord8 10+ putWord8 22+ putWord8 31+ putByteString (B.replicate 31 0)+ putByteString (B.replicate 32 0)+ putWord8 0+ case runGet (get :: Get Pkt) encoded of+ Right BrokenPacketPkt {} -> return ()+ other ->+ assertFailure+ ( "Expected invalid OPS6 salt size to produce BrokenPacketPkt, got "+ ++ show other+ )++testOPS3RejectsInvalidNestedFlagOctet :: Assertion+testOPS3RejectsInvalidNestedFlagOctet = do+ let encoded =+ runPut $ do+ putWord8 0xc4+ putWord8 13+ putWord8 3+ putWord8 0+ putWord8 8+ putWord8 1+ putByteString (B.replicate 8 0)+ putWord8 2+ case runGet (get :: Get Pkt) encoded of+ Right BrokenPacketPkt {} -> pure ()+ other ->+ assertFailure+ ( "Expected invalid OPS3 nested-flag octet to produce BrokenPacketPkt, got "+ ++ show other+ )++testOPS6RejectsInvalidNestedFlagOctet :: Assertion+testOPS6RejectsInvalidNestedFlagOctet = do+ let encoded =+ runPut $ do+ putWord8 0xc4+ putWord8 70+ putWord8 6+ putWord8 0+ putWord8 8+ putWord8 1+ putWord8 32+ putByteString (B.replicate 32 0)+ putByteString (B.replicate 32 0)+ putWord8 2+ case runGet (get :: Get Pkt) encoded of+ Right BrokenPacketPkt {} -> pure ()+ other ->+ assertFailure+ ( "Expected invalid OPS6 nested-flag octet to produce BrokenPacketPkt, got "+ ++ show other+ )++mkECDHBoundaryTestPacket :: Pkt+mkECDHBoundaryTestPacket =+ PublicKeyPkt+ ( PKPayload+ V4+ 0+ 0+ ECDH+ ( ECDHPubKey+ ( ECDSAPubKey+ ( ECDSA_PublicKey+ ( ECDSA.PublicKey+ (ECCT.getCurveByName ECCT.SEC_p256r1)+ (ECCT.Point 1 2)+ )+ )+ )+ SHA256+ AES128+ )+ )++setStrictByteAt+ :: Int -> Word8 -> B.ByteString -> Maybe B.ByteString+setStrictByteAt idx w bs+ | idx < 0 || idx >= B.length bs = Nothing+ | otherwise =+ let (prefix, rest) = B.splitAt idx bs+ in case B.uncons rest of+ Nothing -> Nothing+ Just (_, suffix) -> Just (prefix <> B.singleton w <> suffix)++testECDHPubkeyRejectsReservedKDFLengthZero :: Assertion+testECDHPubkeyRejectsReservedKDFLengthZero = do+ let encoded = BL.toStrict (runPut (put mkECDHBoundaryTestPacket))+ mutated <-+ maybe+ ( assertFailure+ "failed to locate ECDH KDF length byte for zero-length rejection test"+ >> fail "unreachable"+ )+ pure+ (setStrictByteAt (B.length encoded - 4) 0x00 encoded)+ case runGet (get :: Get Pkt) (BL.fromStrict mutated) of+ Right BrokenPacketPkt {} -> pure ()+ other ->+ assertFailure+ ( "Expected ECDH KDF length 0 to produce BrokenPacketPkt, got "+ ++ show other+ )++testECDHPubkeyRejectsReservedKDFLength255 :: Assertion+testECDHPubkeyRejectsReservedKDFLength255 = do+ let encoded = BL.toStrict (runPut (put mkECDHBoundaryTestPacket))+ mutated <-+ maybe+ ( assertFailure+ "failed to locate ECDH KDF length byte for 0xff rejection test"+ >> fail "unreachable"+ )+ pure+ (setStrictByteAt (B.length encoded - 4) 0xff encoded)+ case runGet (get :: Get Pkt) (BL.fromStrict mutated) of+ Right BrokenPacketPkt {} -> pure ()+ other ->+ assertFailure+ ( "Expected ECDH KDF length 255 to produce BrokenPacketPkt, got "+ ++ show other+ )++testECDHPubkeyEncodesFixedKDFLength :: Assertion+testECDHPubkeyEncodesFixedKDFLength = do+ let encoded = BL.toStrict (runPut (put mkECDHBoundaryTestPacket))+ trailer = B.drop (B.length encoded - 4) encoded+ assertEqual+ "ECDH public-key encoding should emit fixed KDF trailer [3,1,hash,sym]"+ (B.pack [0x03, 0x01, fromFVal SHA256, fromFVal AES128])+ trailer++testKeyFlagsSubpacketEncodesEmptySetWithZeroOctet :: Assertion+testKeyFlagsSubpacketEncodesEmptySetWithZeroOctet = do+ let encoded = runPut (put (SigSubPacket False (KeyFlags Set.empty)))+ assertEqual+ "empty key-flags subpacket should encode an explicit zero flags octet"+ [2, 27, 0]+ (BL.unpack encoded)+ case runGet (get :: Get SigSubPacket) encoded of+ Right (SigSubPacket False (KeyFlags flags)) ->+ assertEqual+ "empty key-flags subpacket should decode back to an empty flag set"+ Set.empty+ flags+ other ->+ assertFailure+ ( "Expected empty key-flags subpacket roundtrip, got "+ ++ show other+ )++expectedV6SaltSizeForTest :: HashAlgorithm -> Maybe Int+expectedV6SaltSizeForTest =+ fmap fromIntegral . signatureV6SaltSizeForHashAlgorithm++signatureHasIssuerFingerprintV6+ :: Fingerprint -> SignaturePayload -> Bool+signatureHasIssuerFingerprintV6 expectedFp (SigV6 _ _ _ _ hashed unhashed _ _) =+ expectedFp+ `elem` [ ifp+ | SigSubPacket _ (IssuerFingerprint IssuerFingerprintV6 ifp) <-+ hashed ++ unhashed+ ]+signatureHasIssuerFingerprintV6 _ _ = False++testV6SecretFixtureSignatureSemantics :: Assertion+testV6SecretFixtureSignatureSemantics = do+ armors <- loadArmor "v6-secret.pgp.aa"+ payload <-+ case armors of+ (a : _) -> pure (armorPayload a)+ [] ->+ assertFailure+ "v6-secret.pgp.aa should contain one armored payload"+ >> fail "expected one armored payload"+ let packets = parsePkts payload+ primaryV6Key =+ listToMaybe+ [ pkp+ | SecretKeyPkt pkp _ <- packets+ , _keyVersion pkp == V6+ ]+ <|> listToMaybe+ [ pkp+ | PublicKeyPkt pkp <- packets+ , _keyVersion pkp == V6+ ]+ signatures =+ [ (sig, ha, salt)+ | SignaturePkt sig@(SigV6 _ _ ha salt _ _ _ _) <- packets+ ]+ pkp <-+ case primaryV6Key of+ Nothing ->+ assertFailure+ "v6-secret.pgp.aa should contain a primary v6 key packet"+ >> fail "expected primary v6 key packet"+ Just k -> pure k+ assertBool+ "v6-secret.pgp.aa should contain at least one SigV6 packet"+ (not (null signatures))+ mapM_+ ( \(_, ha, salt) ->+ case expectedV6SaltSizeForTest ha of+ Nothing ->+ assertFailure+ ( "SigV6 in v6-secret.pgp.aa uses unsupported salt hash algorithm: "+ ++ show ha+ )+ Just expected ->+ assertEqual+ "SigV6 salt size in v6-secret.pgp.aa should match hash algorithm"+ expected+ (fromIntegral (BL.length (unSignatureSalt salt)))+ )+ signatures+ assertBool+ "v6-secret.pgp.aa should include at least one IssuerFingerprint v6 matching the primary key"+ ( any+ ( \(sig, _, _) -> signatureHasIssuerFingerprintV6 (fingerprint pkp) sig+ )+ signatures+ )++testV6SecretFixtureDerivesEightOctetKeyID :: Assertion+testV6SecretFixtureDerivesEightOctetKeyID = do+ armors <- loadArmor "v6-secret.pgp.aa"+ payload <-+ case armors of+ (a : _) -> pure (armorPayload a)+ [] ->+ assertFailure+ "v6-secret.pgp.aa should contain one armored payload"+ >> fail "expected one armored payload"+ let packets = parsePkts payload+ primaryV6Key =+ listToMaybe+ [ pkp+ | SecretKeyPkt pkp _ <- packets+ , _keyVersion pkp == V6+ ]+ <|> listToMaybe+ [ pkp+ | PublicKeyPkt pkp <- packets+ , _keyVersion pkp == V6+ ]+ pkp <-+ case primaryV6Key of+ Nothing ->+ assertFailure+ "v6-secret.pgp.aa should contain a primary v6 key packet"+ >> fail "expected primary v6 key packet"+ Just k -> pure k+ derivedKeyId <-+ case eightOctetKeyID pkp of+ Left err ->+ assertFailure+ ("Expected v6 eight-octet key-id derivation to succeed: " ++ err)+ >> fail "expected v6 eight-octet key-id"+ Right keyId -> pure keyId+ let expectedKeyId = EightOctetKeyId (BL.take 8 (unFingerprint (fingerprint pkp)))+ assertEqual+ "v6 eight-octet key-id should be the high-order 64 bits of the fingerprint"+ expectedKeyId+ derivedKeyId
tests/Tests/Utilities.hs view
@@ -2,1354 +2,1622 @@ -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file).--{-# LANGUAGE OverloadedStrings #-}--module Tests.Utilities (utilityTests) where--import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)-import Codec.Encryption.OpenPGP.KeyInfo (pkalgoAbbrev)-import Codec.Encryption.OpenPGP.KeyringParser- ( parsePublicTKs- , parseSecretTKs- , parseTKsEither- , parseTKs- , parseTKsWithWireRep- , parseUnknownTKs- )-import Codec.Encryption.OpenPGP.Serialize- ( PktParseError(..)- , WireRepInput(..)- , conduitParsePktsWithWireRep- , dearmorIfAsciiArmored- , dearmorIfAsciiArmoredLenient- , looksLikeAsciiArmor- , parsePkts- , parsePktsEither- , parsePktsWithWireRep- , wireRepRefFromInput- )-import Codec.Encryption.OpenPGP.Types-import Crypto.Number.Serialize (os2ip)-import Data.Binary (get)-import Data.Binary.Get (Get)-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import qualified Data.Conduit as DC-import qualified Data.Conduit.Binary as CB-import qualified Data.Conduit.List as CL-import Data.Conduit.OpenPGP.Keyring- ( AuthSecretSubkeyRejectionReason(..)- , AuthSecretSubkeyUID(..)- , AuthSecretSubkeysAtReport(..)- , authSecretSubkeysAt- , authSecretSubkeysAtReport- , authSecretSubkeyPrimaryUID- , authSecretSubkeyRejectedReason- , authSecretSubkeyUIDs- , authSecretSubkeyValue- , conduitToSomeTKsDroppingEither- , conduitToAuthSecretSubkeysAt- , conduitToAuthSecretSubkeysAtReport- , conduitToSomeTKsEither- , conduitToPublicTKs- , conduitToSecretTKs- , conduitToTKs- , conduitToTKsWithWireRep- , conduitToUnknownTKs- )-import Data.Conduit.Serialization.Binary (conduitGet)-import Data.Either (lefts, rights)-import Data.List (isInfixOf, nub, sortOn)-import Data.List.NonEmpty (NonEmpty(..))-import Data.Maybe (catMaybes)-import qualified Data.Set as Set-import Test.Tasty (TestTree, testGroup)-import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, testCase)-import Tests.Common- ( addTimestampSeconds- , loadAndDecompressPkts- , loadUnencryptedRsaSigner- , loadV6UnencryptedSecretKeyFixtureForProperty- , readFixtureLazy- , readFixturePackets- , runGet- , setKeyTimestamp- , signCertificationAt- , signSubkeyBindingWithRSAExtrasAt- , signSubkeyRevocationWithRSAAt- , timestampToUTCTime- )---utilityTests :: TestTree-utilityTests =- testGroup- "Utility function group"- [ testCase "pubring as packets" (testParsePktsUtil "pubring.gpg")- , testCase- "pubring parsePktsEither equals parsePkts on valid input"- (testParsePktsEitherUtil "pubring.gpg")- , testCase- "parsePktsEither reports truncation errors"- (testParsePktsEitherFailureUtil "pubring.gpg")- , testCase- "parseUnknownTKs drops disallowed primary-key signature context (v4)"- testParseTKsDropsDisallowedPrimaryKeySigContextV4- , testCase- "parseUnknownTKs drops disallowed primary-key signature context (v6)"- testParseTKsDropsDisallowedPrimaryKeySigContextV6- , testCase- "parseUnknownTKs accepts allowed primary-key signature context (v6)"- testParseTKsAcceptsAllowedPrimaryKeySigContextV6- , testCase "pubring as TKs" (testParseTKsUtil "pubring.gpg")- , testCase- "pubring as typed TKs"- (testParseTKsTypedUtil "pubring.gpg")- , testCase- "pubring parseTKsEither preserves typed parse outcomes"- (testParseTKsEitherUtil "pubring.gpg")- , testCase- "typed TKUnknown conduit partitioning"- (testConduitToTKsTypedUtil "pubring.gpg")- , testCase- "typed TK conduit either reports values without silent drops on valid input"- (testConduitToSomeTKsEitherUtil "pubring.gpg")- , testCase- "typed TK dropping conduit either preserves valid typed results"- (testConduitToSomeTKsDroppingEitherUtil "pubring.gpg")- , testCase- "auth-capable secret-subkey conduit filters revoked/expired/auth flags"- testConduitToAuthSecretSubkeysAtFiltersByValidityAndAuth- , testCase- "auth-capable secret-subkey conduit tracks primary UID over time"- testConduitToAuthSecretSubkeysAtTracksPrimaryUIDOverTime- , testCase- "auth-capable secret-subkey report includes typed rejection reasons"- testAuthSecretSubkeysAtReportIncludesRejectionReasons- , testCase- "pkalgoAbbrev handles RFC9580-era pubkey algorithms"- testPKAlgoAbbrevRFC9580- , testCase- "EightOctetKeyId Show/Read roundtrip uses hex form"- testEightOctetKeyIdReadShowRoundtrip- , testCase- "PktWithWireRep Ord uses raw bytes for tie-breakers"- testPktWithWireRepOrdUsesRawBytes- , testCase- "pubring as packets with provenance"- (testParsePktsWithWireRepUtil "pubring.gpg")- , testCase- "pubring as TKs with provenance"- (testParseTKsWithWireRepUtil "pubring.gpg")- , testCase- "wire provenance tracks original ASCII armor"- testWireRepRefTracksArmorProvenance- , testCase- "dearmorIfAsciiArmored rejects multi-block armored inputs"- testDearmorRejectsMultipleBlocks- , testCase- "dearmorIfAsciiArmoredLenient accepts BOM-prefixed armor"- testDearmorLenientAcceptsBomPrefixedArmor- , testCase- "looksLikeAsciiArmor centralizes armor prefix detection"- testLooksLikeAsciiArmor- , testCase- "wireRepRefFromInput surfaces malformed armored decode errors"- testWireRepRefRejectsMalformedArmoredInput- , testCase- "tksFromWireRep matches any source in TKUnknown provenance list"- testTksFromWireRepMatchesAnySource- , testCase- "TKWithWireRep Semigroup preserves structured provenance"- testSemigroupTKWithWireRepPreservesStructuredRefs- , testCase- "canonicalizeTKWithWireRep matches manual wire-byte ordering"- testCanonicalizeTKWithWireRepMatchesManualWireOrdering- , testCase- "canonicalizeTKWithWireRep reports missing packet refs"- testCanonicalizeTKWithWireRepReportsMissingRef- , testCase- "KeyPkt wrappers preserve packet kind/role round-trips"- testKeyPktWrappersRoundTrip- , testCase- "TK public/secret conversion and projection round-trip"- testTKTypedRoundTripAndPublicView- , testCase- "uat.gpg embeds expected image data from uat.jpg"- testUatImageFixture- ]--testPKAlgoAbbrevRFC9580 :: Assertion-testPKAlgoAbbrevRFC9580 = do- assertEqual "X25519 abbreviation" "x25" (pkalgoAbbrev (toFVal 25))- assertEqual "X448 abbreviation" "x448" (pkalgoAbbrev (toFVal 26))- assertEqual "Ed25519 abbreviation" "e25" (pkalgoAbbrev (toFVal 27))- assertEqual "Ed448 abbreviation" "e448" (pkalgoAbbrev (toFVal 28))--testEightOctetKeyIdReadShowRoundtrip :: Assertion-testEightOctetKeyIdReadShowRoundtrip = do- let eoki = EightOctetKeyId (BL.pack [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef])- assertEqual "show prints canonical uppercase hex" "0123456789ABCDEF" (show eoki)- assertEqual "read . show roundtrip" eoki (read (show eoki))--testUatImageFixture :: Assertion-testUatImageFixture = do- expectedImage <- readFixtureLazy "uat.jpg"- packets <- loadAndDecompressPkts "uat.gpg"- let images =- [ imageData- | UserAttributePkt uas <- packets- , ImageAttribute _ imageData <- uas- ]- assertBool "uat.gpg should contain a user-attribute image packet" (not (null images))- assertBool- "uat.gpg should embed the uat.jpg payload"- (expectedImage `elem` images)--testParsePktsUtil :: FilePath -> Assertion-testParsePktsUtil fn = do- cp <- readFixturePackets fn- pp <- parsePkts `fmap` readFixtureLazy fn- assertEqual- "parsePkts utility function gives same results as conduit pipeline"- cp- pp--testParsePktsEitherUtil :: FilePath -> Assertion-testParsePktsEitherUtil fn = do- lbs <- readFixtureLazy fn- assertEqual- "parsePktsEither matches parsePkts on valid input"- (Right (parsePkts lbs))- (parsePktsEither lbs)--testParsePktsEitherFailureUtil :: FilePath -> Assertion-testParsePktsEitherFailureUtil fn = do- lbs <- readFixtureLazy fn- let truncated = BL.take (BL.length lbs - 1) lbs- case parsePktsEither truncated of- Left (PktParseError off msg) -> do- assertBool "parsePktsEither reports non-empty parse error messages" (not (null msg))- assertBool "parsePktsEither reports an in-range failure offset" (off >= 0 && off <= BL.length truncated)- assertBool- "legacy parsePkts still returns a parsed prefix on malformed input"- (length (parsePkts truncated) <= length (parsePkts lbs))- Right _ ->- assertFailure "parsePktsEither should fail when input is truncated"--testParseTKsUtil :: FilePath -> Assertion-testParseTKsUtil fn = do- lbs <- readFixtureLazy fn- cp <-- DC.runConduitRes $- CB.sourceLbs lbs DC..| conduitGet get DC..| conduitToUnknownTKs DC..| CL.consume- let pt = parseUnknownTKs True . parsePkts $ lbs- assertEqual- "parsePkts utility function gives same results as conduit pipeline"- cp- pt--testParseTKsTypedUtil :: FilePath -> Assertion-testParseTKsTypedUtil fn = do- lbs <- readFixtureLazy fn- let packets = parsePkts lbs- plain = parseUnknownTKs True packets- typed = parseTKs True packets- typedPublic = parsePublicTKs True packets- typedSecret = parseSecretTKs True packets- assertEqual- "parseTKs round-trips to the same untyped TKUnknown semantics"- plain- (map someTKToUnknown typed)- assertEqual- "public + secret typed partitions preserve full typed parse count"- (length typed)- (length typedPublic + length typedSecret)--testParseTKsEitherUtil :: FilePath -> Assertion-testParseTKsEitherUtil fn = do- lbs <- readFixtureLazy fn- let packets = parsePkts lbs- typed = parseTKs True packets- typedEither = parseTKsEither True packets- assertEqual- "parseTKsEither right results should match parseTKs"- typed- (rights typedEither)- assertBool- "parseTKsEither should have no conversion failures for canonical pubring fixture"- (null (lefts typedEither))--testConduitToTKsTypedUtil :: FilePath -> Assertion-testConduitToTKsTypedUtil fn = do- lbs <- readFixtureLazy fn- allTyped <-- DC.runConduitRes $- CB.sourceLbs lbs DC..| conduitGet get DC..| conduitToTKs DC..| CL.consume- publicTyped <-- DC.runConduitRes $- CB.sourceLbs lbs DC..| conduitGet get DC..| conduitToPublicTKs DC..| CL.consume- secretTyped <-- DC.runConduitRes $- CB.sourceLbs lbs DC..| conduitGet get DC..| conduitToSecretTKs DC..| CL.consume- assertEqual- "typed conduit round-trips to parseUnknownTKs semantics"- (parseUnknownTKs True (parsePkts lbs))- (map someTKToUnknown allTyped)- assertEqual- "typed conduit public + secret partitions preserve full count"- (length allTyped)- (length publicTyped + length secretTyped)--testConduitToSomeTKsEitherUtil :: FilePath -> Assertion-testConduitToSomeTKsEitherUtil fn = do- lbs <- readFixtureLazy fn- results <-- DC.runConduitRes $- CB.sourceLbs lbs DC..| conduitGet get DC..| conduitToSomeTKsEither DC..| CL.consume- let typedFromEither = catMaybes (rights results)- assertBool- "conduitToSomeTKsEither should not report failures for canonical pubring fixture"- (null (lefts results))- assertEqual- "conduitToSomeTKsEither right values should match conduitToTKs semantics"- (parseTKs True (parsePkts lbs))- typedFromEither--testConduitToSomeTKsDroppingEitherUtil :: FilePath -> Assertion-testConduitToSomeTKsDroppingEitherUtil fn = do- lbs <- readFixtureLazy fn- results <-- DC.runConduitRes $- CB.sourceLbs lbs DC..| conduitGet get DC..| conduitToSomeTKsDroppingEither DC..| CL.consume- let typedFromEither = catMaybes (rights results)- assertBool- "conduitToSomeTKsDroppingEither should not report failures for canonical pubring fixture"- (null (lefts results))- assertEqual- "conduitToSomeTKsDroppingEither right values should match tolerant parseTKs semantics"- (parseTKs False (parsePkts lbs))- typedFromEither--testConduitToAuthSecretSubkeysAtFiltersByValidityAndAuth :: Assertion-testConduitToAuthSecretSubkeysAtFiltersByValidityAndAuth = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let baseTime = _timestamp signer- beforeTime = timestampToUTCTime (addTimestampSeconds baseTime 18)- afterTime = timestampToUTCTime (addTimestampSeconds baseTime 24)- uidText = "auth-subkeys@example.org"- uid = UserId uidText- secretAddendum = SUUnencrypted (RSAPrivateKey (RSA_PrivateKey signingKey)) 0- authSubkey = setKeyTimestamp (addTimestampSeconds baseTime 1) signer- expiringAuthSubkey = setKeyTimestamp (addTimestampSeconds baseTime 2) signer- signingOnlySubkey = setKeyTimestamp (addTimestampSeconds baseTime 3) signer-- uidCertification <-- signCertificationAt- signer- signingKey- uid- (addTimestampSeconds baseTime 8)- [SigSubPacket False (PrimaryUserId True)]- authBinding <-- signSubkeyBindingWithRSAExtrasAt- signer- authSubkey- signingKey- (addTimestampSeconds baseTime 10)- [SigSubPacket False (KeyFlags (Set.fromList [AuthKey]))]- expiringAuthBinding <-- signSubkeyBindingWithRSAExtrasAt- signer- expiringAuthSubkey- signingKey- (addTimestampSeconds baseTime 10)- [ SigSubPacket False (KeyFlags (Set.fromList [AuthKey]))- , SigSubPacket False (KeyExpirationTime 20)- ]- signingOnlyBinding <-- signSubkeyBindingWithRSAExtrasAt- signer- signingOnlySubkey- signingKey- (addTimestampSeconds baseTime 10)- [SigSubPacket False (KeyFlags (Set.fromList [SignDataKey]))]- authRevocation <-- signSubkeyRevocationWithRSAAt- signer- authSubkey- signingKey- (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"-- selectedBefore <-- DC.runConduitRes $- CL.sourceList [typedSecret] DC..| conduitToAuthSecretSubkeysAt beforeTime DC..| CL.consume- assertEqual- "pure authSecretSubkeysAt helper and conduit output should match"- (authSecretSubkeysAt beforeTime typedSecret)- selectedBefore- assertEqual- "before revocation/expiry, conduit should keep only auth-capable secret subkeys"- 2- (length selectedBefore)- let selectedBeforeFps =- Set.fromList- (map (fingerprint . keyPktPKPayload . authSecretSubkeyValue) selectedBefore)- assertEqual- "selected auth-capable subkeys should match expected fingerprints"- (Set.fromList [fingerprint authSubkey, fingerprint expiringAuthSubkey])- selectedBeforeFps- mapM_- (\selection -> do- assertEqual "selected auth subkey should retain active primary UID context" (Just uidText) (authSecretSubkeyPrimaryUID selection)- assertEqual- "selected auth subkey should expose UID context with primary marker"- [AuthSecretSubkeyUID uidText True]- (authSecretSubkeyUIDs selection))- selectedBefore-- selectedAfter <-- DC.runConduitRes $- CL.sourceList [typedSecret] DC..| conduitToAuthSecretSubkeysAt afterTime DC..| CL.consume- assertEqual- "after revocation/expiry, conduit should drop revoked/expired auth subkeys"- []- selectedAfter--testConduitToAuthSecretSubkeysAtTracksPrimaryUIDOverTime :: Assertion-testConduitToAuthSecretSubkeysAtTracksPrimaryUIDOverTime = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let baseTime = _timestamp signer- beforeTime = timestampToUTCTime (addTimestampSeconds baseTime 15)- afterTime = timestampToUTCTime (addTimestampSeconds baseTime 25)- uidA = UserId "uid-a@example.org"- UserId uidAText = uidA- uidB = UserId "uid-b@example.org"- UserId uidBText = uidB- secretAddendum = SUUnencrypted (RSAPrivateKey (RSA_PrivateKey signingKey)) 0- authSubkey = setKeyTimestamp (addTimestampSeconds baseTime 1) signer-- uidACert <-- signCertificationAt- signer- signingKey- uidA- (addTimestampSeconds baseTime 10)- [SigSubPacket False (PrimaryUserId True)]- uidBCert <-- signCertificationAt- signer- signingKey- uidB- (addTimestampSeconds baseTime 20)- [SigSubPacket False (PrimaryUserId True)]- authBinding <-- signSubkeyBindingWithRSAExtrasAt- signer- authSubkey- signingKey- (addTimestampSeconds baseTime 11)- [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"-- beforeSelections <-- DC.runConduitRes $- CL.sourceList [typedSecret] DC..| conduitToAuthSecretSubkeysAt beforeTime DC..| CL.consume- beforeSelection <-- case beforeSelections of- [selection] -> pure selection- other ->- assertFailure ("expected one auth-capable subkey selection before primary-uid rollover, got " ++ show (length other)) >>- fail "unreachable"- assertEqual- "earlier primary UID should be selected before newer self-certification"- (Just uidAText)- (authSecretSubkeyPrimaryUID beforeSelection)- assertEqual- "UID context should include only active UIDs before uid-b certification exists"- [(uidAText, True)]- (map (\u -> (authSecretSubkeyUIDValue u, authSecretSubkeyUIDIsPrimary u)) (authSecretSubkeyUIDs beforeSelection))-- afterSelections <-- DC.runConduitRes $- CL.sourceList [typedSecret] DC..| conduitToAuthSecretSubkeysAt afterTime DC..| CL.consume- afterSelection <-- case afterSelections of- [selection] -> pure selection- other ->- assertFailure ("expected one auth-capable subkey selection after primary-uid rollover, got " ++ show (length other)) >>- fail "unreachable"- assertEqual- "newer primary UID self-certification should win after rollover"- (Just uidBText)- (authSecretSubkeyPrimaryUID afterSelection)- assertEqual- "UID context should mark uid-b as primary after rollover"- [(uidAText, False), (uidBText, True)]- (map (\u -> (authSecretSubkeyUIDValue u, authSecretSubkeyUIDIsPrimary u)) (authSecretSubkeyUIDs afterSelection))--testAuthSecretSubkeysAtReportIncludesRejectionReasons :: Assertion-testAuthSecretSubkeysAtReportIncludesRejectionReasons = do- (signer, signingKey) <- loadUnencryptedRsaSigner- let baseTime = _timestamp signer- beforeTime = timestampToUTCTime (addTimestampSeconds baseTime 18)- afterTime = timestampToUTCTime (addTimestampSeconds baseTime 24)- uidText = "auth-subkeys@example.org"- uid = UserId uidText- secretAddendum = SUUnencrypted (RSAPrivateKey (RSA_PrivateKey signingKey)) 0- authSubkey = setKeyTimestamp (addTimestampSeconds baseTime 1) signer- expiringAuthSubkey = setKeyTimestamp (addTimestampSeconds baseTime 2) signer- signingOnlySubkey = setKeyTimestamp (addTimestampSeconds baseTime 3) signer-- uidCertification <-- signCertificationAt- signer- signingKey- uid- (addTimestampSeconds baseTime 8)- [SigSubPacket False (PrimaryUserId True)]- authBinding <-- signSubkeyBindingWithRSAExtrasAt- signer- authSubkey- signingKey- (addTimestampSeconds baseTime 10)- [SigSubPacket False (KeyFlags (Set.fromList [AuthKey]))]- expiringAuthBinding <-- signSubkeyBindingWithRSAExtrasAt- signer- expiringAuthSubkey- signingKey- (addTimestampSeconds baseTime 10)- [ SigSubPacket False (KeyFlags (Set.fromList [AuthKey]))- , SigSubPacket False (KeyExpirationTime 20)- ]- signingOnlyBinding <-- signSubkeyBindingWithRSAExtrasAt- signer- signingOnlySubkey- signingKey- (addTimestampSeconds baseTime 10)- [SigSubPacket False (KeyFlags (Set.fromList [SignDataKey]))]- authRevocation <-- signSubkeyRevocationWithRSAAt- signer- authSubkey- signingKey- (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"-- let beforeReport = authSecretSubkeysAtReport beforeTime typedSecret- assertEqual- "report accepted list should match authSecretSubkeysAt before revocation/expiry"- (authSecretSubkeysAt beforeTime typedSecret)- (authSecretSubkeysAccepted beforeReport)- assertEqual- "before revocation/expiry, only signing-only subkey should be rejected"- [AuthSecretSubkeyMissingAuthCapability]- (map authSecretSubkeyRejectedReason (authSecretSubkeysRejected beforeReport))-- afterReportsViaConduit <-- DC.runConduitRes $- CL.sourceList [typedSecret] DC..| conduitToAuthSecretSubkeysAtReport afterTime DC..| CL.consume- afterReport <-- case afterReportsViaConduit of- [singleReport] -> pure singleReport- other ->- assertFailure ("expected one report from conduitToAuthSecretSubkeysAtReport, got " ++ show (length other)) >>- fail "unreachable"- assertEqual- "after revocation/expiry, no accepted auth subkeys should remain"- []- (authSecretSubkeysAccepted afterReport)- assertEqual- "after revocation/expiry, rejected reasons should report invalid-time and missing-auth cases for still-visible candidates"- [ AuthSecretSubkeySubkeyInvalidAtTime- , AuthSecretSubkeyMissingAuthCapability- ]- (map authSecretSubkeyRejectedReason (authSecretSubkeysRejected afterReport))--testPktWithWireRepOrdUsesRawBytes :: Assertion-testPktWithWireRepOrdUsesRawBytes = do- let src = wireRepRef "src"- pktA =- PktWithWireRep- src- (ByteRange 0 1)- "a"- 0- (OtherPacketPkt 42 "a")- pktB =- PktWithWireRep- src- (ByteRange 1 1)- "b"- 1- (BrokenPacketPkt "broken" 42 "b")- assertEqual- "base packet ordering can tie on tag-only fallback"- EQ- (compare (_pktValue pktA) (_pktValue pktB))- assertEqual- "PktWithWireRep ordering should break ties using packet bytes"- LT- (compare pktA pktB)--testParsePktsWithWireRepUtil :: FilePath -> Assertion-testParsePktsWithWireRepUtil fn = do- let fpath = "tests/data/" ++ fn- lbs <- BL.readFile fpath- let WireRepInput- { wireRepInputRef = src- , wireRepInputPayload = srcBytes- } =- either- (const- WireRepInput- { wireRepInputRef = wireRepRef lbs- , wireRepInputPayload = lbs- })- id- (wireRepRefFromInput Nothing lbs)- parsed = parsePktsWithWireRep src srcBytes- chunkedInput = chunkStrict 7 (BL.toStrict lbs)- conduitParsed <-- DC.runConduitRes $- CB.sourceLbs lbs DC..| conduitParsePktsWithWireRep Nothing DC..| CL.consume- conduitParsedChunked <-- DC.runConduitRes $- CL.sourceList chunkedInput DC..| conduitParsePktsWithWireRep Nothing DC..| CL.consume- assertEqual- "provenance-aware parsePkts preserves packet semantics"- (parsePkts lbs)- (map _pktValue parsed)- assertEqual- "conduit and pure provenance-aware packet parsing agree"- parsed- conduitParsed- assertEqual- "conduit parser handles packets split across chunk boundaries"- parsed- conduitParsedChunked- assertEqual- "packetsFromWireRep returns all packets for the originating bytestream"- parsed- (packetsFromWireRep src parsed)- mapM_ (assertPktProvenance src srcBytes) parsed- where- chunkStrict n bs- | B.null bs = []- | otherwise =- let (prefix, suffix) = B.splitAt n bs- in prefix : chunkStrict n suffix--testParseTKsWithWireRepUtil :: FilePath -> Assertion-testParseTKsWithWireRepUtil fn = do- let fpath = "tests/data/" ++ fn- lbs <- BL.readFile fpath- let WireRepInput- { wireRepInputRef = src- , wireRepInputPayload = srcBytes- } =- either- (const- WireRepInput- { wireRepInputRef = wireRepRef lbs- , wireRepInputPayload = lbs- })- id- (wireRepRefFromInput Nothing lbs)- packets = parsePktsWithWireRep src srcBytes- parsed = parseTKsWithWireRep True packets- plain = parseUnknownTKs True (map _pktValue packets)- conduitParsed <-- DC.runConduitRes $ CL.sourceList packets DC..| conduitToTKsWithWireRep DC..| CL.consume- assertEqual- "provenance-aware parseUnknownTKs preserves TKUnknown semantics"- plain- (map _tkValue parsed)- assertEqual- "conduit and pure provenance-aware TKUnknown parsing agree"- parsed- conduitParsed- assertEqual- "tksFromWireRep returns all TKs for the originating bytestream"- parsed- (tksFromWireRep src parsed)- mapM_ (assertTKProvenance src parsed) parsed--assertPktProvenance :: WireRepRef -> BL.ByteString -> PktWithWireRep -> Assertion-assertPktProvenance src srcBytes pkt = do- assertEqual- "packet raw bytes round-trip back to the same packet"- (Right (_pktValue pkt))- (runGet (get :: Get Pkt) (_pktRaw pkt))- assertEqual- "packet source reference is preserved"- src- (wireRepOfPkt pkt)- let ByteRange offset len = _pktRange pkt- assertEqual- "packet raw bytes match the source bytestream slice"- (_pktRaw pkt)- (BL.take len (BL.drop offset srcBytes))--assertTKProvenance :: WireRepRef -> [TKWithWireRep] -> TKWithWireRep -> Assertion-assertTKProvenance src allTks tk = do- assertBool "TKUnknown source reference list includes originating source" (src `elem` _tkWireRepRefs tk)- assertEqual "TKUnknown source reference is preserved" src (wireRepOfTK tk)- assertEqual- "TKUnknown packet references reconstruct the semantic TKUnknown packet sequence"- (flattenTK (_tkValue tk))- (map _pktValue (packetRefsOfTK tk))- assertEqual- "TKUnknown 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"- (tk `elem` tksContainingPacket pkt allTks))- (packetRefsOfTK tk)- case toStructuredTKWithWireRep tk of- Left err ->- assertFailure ("toStructuredTKWithWireRep failed: " ++ err)- Right structured -> do- assertEqual- "structured provenance retains semantic primary key"- (_tkuKey (_tkValue tk))- (_tkStructuredPrimaryKey structured)- assertEqual- "structured provenance retains packet source reference list"- (_tkWireRepRefs tk)- (_tkStructuredWireRepRefs structured)- resolved <- resolveStructuredPacketRefs structured- assertEqual- "structured provenance resolves packet refs in TKUnknown packet order"- (map _pktValue (packetRefsOfTK tk))- (map _pktValue resolved)- assertEqual- "structured provenance keeps stable packet ref ids"- (map packetRefIdOf (packetRefsOfTK tk))- (map packetRefIdOf resolved)--resolveStructuredPacketRefs :: TKStructuredWithWireRep -> IO [PktWithWireRep]-resolveStructuredPacketRefs structured = do- let collectSigRefs = map _signatureWithWireRefRef- uidRefs =- concatMap- (\uid -> _uidWithWireRefsRef uid : collectSigRefs (_uidWithWireRefsSignatures uid))- (_tkStructuredUIDs structured)- uatRefs =- concatMap- (\uat -> _uatWithWireRefsRef uat : collectSigRefs (_uatWithWireRefsSignatures uat))- (_tkStructuredUAts structured)- subRefs =- concatMap- (\sub ->- _subkeyWithWireRefsRef sub : collectSigRefs (_subkeyWithWireRefsSignatures sub))- (_tkStructuredSubkeys structured)- refIds =- _tkStructuredPrimaryKeyRef structured :- collectSigRefs (_tkStructuredDirectSignatures structured) ++ uidRefs ++ uatRefs ++ subRefs- mapM- (\refId ->- case lookupPacketRef structured refId of- Nothing ->- assertFailure- ("lookupPacketRef failed for ref id " ++ show refId)- Just pkt -> pure pkt)- refIds--testCanonicalizeTKWithWireRepMatchesManualWireOrdering :: Assertion-testCanonicalizeTKWithWireRepMatchesManualWireOrdering = do- lbs <- readFixtureLazy "pubring.gpg"- let src = wireRepRef lbs- parsed = parseTKsWithWireRep True (parsePktsWithWireRep src lbs)- case parsed of- (tk:_) ->- case toStructuredTKWithWireRep tk of- Left err ->- assertFailure ("toStructuredTKWithWireRep failed: " ++ err)- Right structured -> do- let shuffled =- structured- { _tkStructuredDirectSignatures =- reverse (_tkStructuredDirectSignatures structured)- , _tkStructuredUIDs =- reverse- (map- (\uid ->- uid- { _uidWithWireRefsSignatures =- reverse (_uidWithWireRefsSignatures uid)- })- (_tkStructuredUIDs structured))- , _tkStructuredUAts =- reverse- (map- (\uat ->- uat- { _uatWithWireRefsSignatures =- reverse (_uatWithWireRefsSignatures uat)- })- (_tkStructuredUAts structured))- , _tkStructuredSubkeys =- reverse- (map- (\sub ->- sub- { _subkeyWithWireRefsSignatures =- reverse (_subkeyWithWireRefsSignatures sub)- })- (_tkStructuredSubkeys structured))- }- expected <-- case manualCanonicalizeStructured shuffled of- Left err ->- assertFailure ("manual canonicalization failed: " ++ err) >> fail "manual canonicalization failed"- Right x -> pure x- got <-- case canonicalizeTKStructuredWithWireRep shuffled of- Left err ->- assertFailure ("canonicalizeTKStructuredWithWireRep failed: " ++ show err) >>- fail "canonicalizeTKStructuredWithWireRep failed"- Right x -> pure x- assertEqual- "canonicalizeTKStructuredWithWireRep matches manual wire-byte ordering"- expected- got- wrapped <-- case canonicalizeTKWithWireRep tk of- Left err ->- assertFailure ("canonicalizeTKWithWireRep failed: " ++ show err) >>- fail "canonicalizeTKWithWireRep failed"- Right x -> pure x- structuredCanonical <-- case canonicalizeTKStructuredWithWireRep structured of- Left err ->- assertFailure ("canonicalizeTKStructuredWithWireRep failed: " ++ show err) >>- fail "canonicalizeTKStructuredWithWireRep failed"- Right x -> pure x- assertEqual- "canonicalizeTKWithWireRep delegates to structured canonicalization"- structuredCanonical- wrapped- [] ->- assertFailure "pubring.gpg should parse to at least one provenance-aware TKUnknown"- where- manualCanonicalizeStructured :: TKStructuredWithWireRep -> Either String TKUnknown- manualCanonicalizeStructured structured = do- direct <- sortSigs (_tkStructuredDirectSignatures structured)- uids <-- sortByRef _uidWithWireRefsRef =<<- mapM- (\uid -> do- sigs <- sortSigs (_uidWithWireRefsSignatures uid)- Right (uid, sigs))- (_tkStructuredUIDs structured)- uats <-- sortByRef _uatWithWireRefsRef =<<- mapM- (\uat -> do- sigs <- sortSigs (_uatWithWireRefsSignatures uat)- Right (uat, sigs))- (_tkStructuredUAts structured)- subs <-- sortByRef _subkeyWithWireRefsRef =<<- mapM- (\sub -> do- sigs <- sortSigs (_subkeyWithWireRefsSignatures sub)- Right (sub, sigs))- (_tkStructuredSubkeys structured)- Right $- TKUnknown- { _tkuKey = _tkStructuredPrimaryKey structured- , _tkuRevs = map _signatureWithWireRefValue direct- , _tkuUIDs =- map- (\(uid, sigs) ->- (_uidWithWireRefsValue uid, map _signatureWithWireRefValue sigs))- uids- , _tkuUAts =- map- (\(uat, sigs) ->- (_uatWithWireRefsValue uat, map _signatureWithWireRefValue sigs))- uats- , _tkuSubs =- map- (\(sub, sigs) ->- (_subkeyWithWireRefsValue sub, map _signatureWithWireRefValue sigs))- subs- }- where- wireBytes refId =- maybe- (Left ("lookupPacketRef failed for ref id " ++ show refId))- (Right . _pktRaw)- (lookupPacketRef structured refId)-- sortSigs sigs = do- keyed <-- mapM- (\sig -> do- raw <- wireBytes (_signatureWithWireRefRef sig)- Right ((raw, _signatureWithWireRefRef sig), sig))- sigs- Right (map snd (sortOn fst keyed))-- sortByRef refAccessor items = do- keyed <-- mapM- (\(x, sigs) -> do- raw <- wireBytes (refAccessor x)- Right ((raw, refAccessor x), (x, sigs)))- items- Right (map snd (sortOn fst keyed))--testCanonicalizeTKWithWireRepReportsMissingRef :: Assertion-testCanonicalizeTKWithWireRepReportsMissingRef = do- lbs <- readFixtureLazy "pubring.gpg"- let src = wireRepRef lbs- parsed = parseTKsWithWireRep True (parsePktsWithWireRep src lbs)- case parsed of- (tk:_) ->- case toStructuredTKWithWireRep tk of- Left err ->- assertFailure ("toStructuredTKWithWireRep failed: " ++ err)- Right structured -> do- let badRef = PacketRefId src 999999- brokenWithBadRef =- case _tkStructuredDirectSignatures structured of- (sig:rest) ->- Just- structured- { _tkStructuredDirectSignatures =- sig {_signatureWithWireRefRef = badRef} : rest- }- [] ->- case _tkStructuredUIDs structured of- (uid:restUIDs) ->- Just- structured- { _tkStructuredUIDs =- uid {_uidWithWireRefsRef = badRef} : restUIDs- }- [] ->- case _tkStructuredUAts structured of- (uat:restUATs) ->- Just- structured- { _tkStructuredUAts =- uat {_uatWithWireRefsRef = badRef} : restUATs- }- [] ->- case _tkStructuredSubkeys structured of- (sub:restSubs) ->- Just- structured- { _tkStructuredSubkeys =- sub {_subkeyWithWireRefsRef = badRef} : restSubs- }- [] -> Nothing- case brokenWithBadRef of- Nothing ->- assertFailure- "pubring.gpg first TKUnknown unexpectedly has no direct signatures, UIDs, UATs, or subkeys"- Just broken ->- case canonicalizeTKStructuredWithWireRep broken of- Left (CanonicalizeMissingPacketRef ref) ->- assertEqual "missing ref error should include unresolved ref id" badRef ref- Left err ->- assertFailure ("Expected CanonicalizeMissingPacketRef, got " ++ show err)- Right _ ->- assertFailure "Expected canonicalization to fail on missing packet ref"- [] ->- assertFailure "pubring.gpg should parse to at least one provenance-aware TKUnknown"--testWireRepRefTracksArmorProvenance :: Assertion-testWireRepRefTracksArmorProvenance = do- armored <- readFixtureLazy "v6-secret.pgp.aa"- case wireRepRefFromInput Nothing armored of- Left err -> assertFailure ("wireRepRefFromInput failed on armored input: " ++ err)- Right WireRepInput- { wireRepInputRef = src- , wireRepInputPayload = payload- } -> do- assertBool- "wireRepRefFromInput should mark ASCII-armored input as originally armored"- (_wireRepWasOriginallyArmored src)- assertBool- "dearmored payload should parse into packets"- (not (null (parsePktsWithWireRep src payload)))--testDearmorRejectsMultipleBlocks :: Assertion-testDearmorRejectsMultipleBlocks = do- armored <- readFixtureLazy "v6-secret.pgp.aa"- case dearmorIfAsciiArmored (armored <> "\n" <> armored) of- Left err ->- assertBool- "multi-block rejection error should mention expected single block"- ("expected exactly one" `isInfixOf` err)- Right _ ->- assertFailure "dearmorIfAsciiArmored unexpectedly accepted multi-block armor input"--testDearmorLenientAcceptsBomPrefixedArmor :: Assertion-testDearmorLenientAcceptsBomPrefixedArmor = do- armored <- readFixtureLazy "v6-secret.pgp.aa"- let bomPrefixed = BL.pack [0xef, 0xbb, 0xbf] <> armored- case dearmorIfAsciiArmored bomPrefixed of- Right (False, _) -> pure ()- Right (True, _) ->- assertFailure- "strict dearmorIfAsciiArmored unexpectedly treated BOM-prefixed input as armored"- Left err ->- assertFailure ("strict dearmorIfAsciiArmored failed unexpectedly: " ++ err)- case dearmorIfAsciiArmoredLenient bomPrefixed of- Left err ->- assertFailure ("lenient dearmor should decode BOM-prefixed armor: " ++ err)- Right (wasArmored, payload) -> do- assertBool "lenient dearmor should report armored input" wasArmored- assertBool "lenient dearmor payload should parse as packets" (not (null (parsePkts payload)))--testLooksLikeAsciiArmor :: Assertion-testLooksLikeAsciiArmor = do- let armoredPrefix = "\n\t -----BEGIN PGP MESSAGE-----\nYWJj\n"- partialPrefix = "-----BEGIN PG"- binaryPrefix = BL.pack [0x99, 0x01, 0x02, 0x03]- assertBool- "looksLikeAsciiArmor accepts canonical armored headers with leading whitespace"- (looksLikeAsciiArmor armoredPrefix)- assertBool- "looksLikeAsciiArmor rejects partial armored headers"- (not (looksLikeAsciiArmor partialPrefix))- assertBool- "looksLikeAsciiArmor rejects binary packet prefixes"- (not (looksLikeAsciiArmor binaryPrefix))--testWireRepRefRejectsMalformedArmoredInput :: Assertion-testWireRepRefRejectsMalformedArmoredInput = do- let malformed =- "-----BEGIN PGP MESSAGE-----\n" <>- "not base64 and no checksum\n" <>- "-----END PGP MESSAGE-----\n"- case wireRepRefFromInput Nothing malformed of- Left _ -> pure ()- Right _ ->- assertFailure- "wireRepRefFromInput unexpectedly accepted malformed ASCII-armored input"--testTksFromWireRepMatchesAnySource :: Assertion-testTksFromWireRepMatchesAnySource = do- lbs <- readFixtureLazy "pubring.gpg"- let srcA = wireRepRef lbs- srcB = namedWireRepRef "synthetic-merge-source" lbs- parsed = parseTKsWithWireRep True (parsePktsWithWireRep srcA lbs)- case parsed of- (tk:_) -> do- let multiSourceTk = tk { _tkWireRepRefs = srcA :| [srcB] }- assertBool- "tksFromWireRep matches TKs whose source list contains the queried source"- (multiSourceTk `elem` tksFromWireRep srcA [multiSourceTk])- assertBool- "tksFromWireRep can match secondary provenance sources"- (multiSourceTk `elem` tksFromWireRep srcB [multiSourceTk])- [] ->- assertFailure "pubring.gpg should parse to at least one provenance-aware TKUnknown"--testSemigroupTKWithWireRepPreservesStructuredRefs :: Assertion-testSemigroupTKWithWireRepPreservesStructuredRefs = do- lbs <- readFixtureLazy "pubring.gpg"- let srcA = wireRepRef lbs- srcB = namedWireRepRef "synthetic-merge-source" lbs- parsed = parseTKsWithWireRep True (parsePktsWithWireRep srcA lbs)- case parsed of- (tk:_) -> do- let remappedPackets = map (\pkt -> pkt { _pktWireRepRef = srcB }) (packetRefsOfTK tk)- tkFromSecondSource =- TKWithWireRep- (srcB :| [])- (spanByteRanges (map _pktRange remappedPackets))- remappedPackets- (_tkValue tk)- merged = tk <> tkFromSecondSource- mergedRefIds = map packetRefIdOf (packetRefsOfTK merged)- assertEqual- "Semigroup preserves TKUnknown semantic merge behavior"- (_tkValue tk <> _tkValue tkFromSecondSource)- (_tkValue merged)- assertBool- "Semigroup merged provenance references include both sources"- (srcA `elem` wireRepsOfTK merged && srcB `elem` wireRepsOfTK merged)- assertEqual- "Semigroup result packet refs match merged TKUnknown packet sequence"- (flattenTK (_tkValue merged))- (map _pktValue (packetRefsOfTK merged))- assertEqual- "Semigroup result keeps packet refs unique by source-aware PacketRefId"- (length mergedRefIds)- (length (nub mergedRefIds))- case toStructuredTKWithWireRep merged of- Left err ->- assertFailure ("toStructuredTKWithWireRep failed for Semigroup result: " ++ err)- Right structured -> do- resolved <- resolveStructuredPacketRefs structured- assertEqual- "Semigroup result structured refs resolve in packet order"- (map _pktValue (packetRefsOfTK merged))- (map _pktValue resolved)- [] ->- assertFailure "pubring.gpg should parse to at least one provenance-aware TKUnknown"--testKeyPktWrappersRoundTrip :: Assertion-testKeyPktWrappersRoundTrip = do- fixture <- loadV6UnencryptedSecretKeyFixtureForProperty- case fixture of- Left err ->- assertFailure err- Right (pkp, ska, _) -> do- let publicPrimaryPkt = PublicKeyPkt pkp- publicSubkeyPkt = PublicSubkeyPkt pkp- secretPrimaryPkt = SecretKeyPkt pkp ska- secretSubkeyPkt = SecretSubkeyPkt pkp ska- assertEqual- "mkPrimaryKeyPkt preserves public primary packets"- publicPrimaryPkt- (someKeyPktToPkt (mkPrimaryKeyPkt pkp Nothing))- assertEqual- "mkPrimaryKeyPkt preserves secret primary packets"- secretPrimaryPkt- (someKeyPktToPkt (mkPrimaryKeyPkt pkp (Just ska)))- assertEqual- "mkSubkeyKeyPkt preserves public subkey packets"- publicSubkeyPkt- (someKeyPktToPkt (mkSubkeyKeyPkt pkp Nothing))- assertEqual- "mkSubkeyKeyPkt preserves secret subkey packets"- secretSubkeyPkt- (someKeyPktToPkt (mkSubkeyKeyPkt pkp (Just ska)))- case pktToPublicKeyPkt publicPrimaryPkt of- Nothing ->- assertFailure "pktToPublicKeyPkt should accept PublicKeyPkt"- Just keyPkt -> do- assertEqual "public primary role is preserved" KeyPktPrimary (keyPktRole keyPkt)- assertEqual "public primary TKUnknown key view is preserved" (pkp, Nothing) (keyPktTKKey keyPkt)- assertEqual "public primary round-trips through KeyPkt" publicPrimaryPkt (keyPktToPkt keyPkt)- case pktToPublicKeyPkt publicSubkeyPkt of- Nothing ->- assertFailure "pktToPublicKeyPkt should accept PublicSubkeyPkt"- Just keyPkt ->- assertEqual "public subkey role is preserved" KeyPktSubkey (keyPktRole keyPkt)- case pktToSecretKeyPkt secretPrimaryPkt of- Nothing ->- assertFailure "pktToSecretKeyPkt should accept SecretKeyPkt"- Just keyPkt -> do- assertEqual "secret primary role is preserved" KeyPktPrimary (keyPktRole keyPkt)- assertEqual "secret primary TKUnknown key view is preserved" (pkp, Just ska) (keyPktTKKey keyPkt)- assertEqual "secret primary round-trips through KeyPkt" secretPrimaryPkt (keyPktToPkt keyPkt)- assertEqual- "secret primary public view downgrades to PublicKeyPkt"- publicPrimaryPkt- (keyPktToPkt (keyPktToPublicView keyPkt))- case pktToSecretKeyPkt secretSubkeyPkt of- Nothing ->- assertFailure "pktToSecretKeyPkt should accept SecretSubkeyPkt"- Just keyPkt -> do- assertEqual "secret subkey role is preserved" KeyPktSubkey (keyPktRole keyPkt)- assertEqual- "secret subkey public view downgrades to PublicSubkeyPkt"- publicSubkeyPkt- (keyPktToPkt (keyPktToPublicView keyPkt))- case pktToSomeKeyPktEither (UserIdPkt "not a key packet") of- Left (NotAKeyPacket pkt) ->- assertEqual "non-key coercion error reports the original packet" (UserIdPkt "not a key packet") pkt- other ->- assertFailure ("Expected NotAKeyPacket error, got " ++ show other)--testTKTypedRoundTripAndPublicView :: Assertion-testTKTypedRoundTripAndPublicView = do- pubringBytes <- readFixtureLazy "pubring.gpg"- let publicParsed = parseUnknownTKs 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" >> fail "unreachable"- assertEqual- "public typed TKUnknown round-trips back to untyped TKUnknown"- publicTk- (someTKToUnknown publicTyped)-- armored <- readFixtureLazy "v6-secret.pgp.aa"- secretTk <- do- payload <-- case dearmorIfAsciiArmored armored of- Left err ->- assertFailure ("failed to decode v6-secret fixture: " ++ err) >> fail "unreachable"- Right (_, bs) -> pure bs- case parseUnknownTKs 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" >> 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)- }- assertEqual- "publicViewTK drops secret material from primary/subkeys"- expectedPublic- projectedPublic--flattenTK :: TKUnknown -> [Pkt]-flattenTK tk =- [someKeyPktToPkt (mkPrimaryKeyPkt pkp mska)] ++- map SignaturePkt (_tkuRevs 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--testParseTKsDropsDisallowedPrimaryKeySigContextV4 :: Assertion-testParseTKsDropsDisallowedPrimaryKeySigContextV4 = do- let pkp =- PKPayload- V4- (ThirtyTwoBitTimeStamp 0)- 0- EdDSA- (EdDSAPubKey Ed25519 (PrefixedNativeEPoint (EPoint (os2ip (B.cons 0x40 (B.replicate 32 0x01))))))- invalidSig = SigV4 GenericCert RSA SHA512 [] [] 0 (MPI 0 :| [])- case parseUnknownTKs True [PublicKeyPkt pkp, SignaturePkt invalidSig] of- [tk] ->- assertEqual- "parseUnknownTKs True should drop GenericCert as a primary-key signature in v4"- []- (_tkuRevs tk)- other ->- assertFailure ("Expected one TKUnknown when dropping invalid v4 signature context, got " ++ show other)--testParseTKsDropsDisallowedPrimaryKeySigContextV6 :: Assertion-testParseTKsDropsDisallowedPrimaryKeySigContextV6 = do- let pkp =- PKPayload- V6- (ThirtyTwoBitTimeStamp 0)- 0- EdDSA- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint 1)))- invalidSig =- SigV6- GenericCert- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x01))- []- []- 0- (MPI 0 :| [])- case parseUnknownTKs True [PublicKeyPkt pkp, SignaturePkt invalidSig] of- [tk] ->- assertEqual- "parseUnknownTKs True should drop GenericCert as a primary-key signature in v6"- []- (_tkuRevs tk)- other ->- assertFailure ("Expected one TKUnknown when dropping invalid v6 signature context, got " ++ show other)--testParseTKsAcceptsAllowedPrimaryKeySigContextV6 :: Assertion-testParseTKsAcceptsAllowedPrimaryKeySigContextV6 = do- let pkp =- PKPayload- V6- (ThirtyTwoBitTimeStamp 0)- 0- EdDSA- (EdDSAPubKey Ed25519 (NativeEPoint (EPoint 1)))- allowedSig =- SigV6- KeyRevocationSig- EdDSA- SHA512- (SignatureSalt (BL.replicate 32 0x02))- []- []- 0- (MPI 0 :| [])- case parseUnknownTKs True [PublicKeyPkt pkp, SignaturePkt allowedSig] of- [tk] ->- assertBool- "parseUnknownTKs True should keep allowed v6 key-revocation signatures on primary keys"- (not (null (_tkuRevs tk)))- other ->- assertFailure- ("Expected one TKUnknown with a retained v6 revocation signature, got " ++ show other)+{-# LANGUAGE OverloadedStrings #-}++module Tests.Utilities (utilityTests) where++import Control.Error.Util (hush)+import Control.Monad (join)+import Crypto.Number.Serialize (os2ip)+import Data.Binary (get)+import Data.Binary.Get (Get)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import qualified Data.Conduit as DC+import qualified Data.Conduit.Binary as CB+import qualified Data.Conduit.List as CL+import Data.Conduit.Serialization.Binary (conduitGet)+import Data.Either (lefts, rights)+import Data.List (isInfixOf, nub, sortOn)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Maybe (catMaybes)+import qualified Data.Set as Set+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit+ ( Assertion+ , assertBool+ , assertEqual+ , assertFailure+ , testCase+ )++import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)+import Codec.Encryption.OpenPGP.KeyInfo (pkalgoAbbrev)+import Codec.Encryption.OpenPGP.KeyringParser+ ( parsePublicTKs+ , parseSecretTKs+ , parseTKs+ , parseTKsEither+ , parseTKsWithWireRep+ , parseUnknownTKs+ )+import Codec.Encryption.OpenPGP.Serialize+ ( PktParseError (..)+ , WireRepInput (..)+ , conduitParsePktsWithWireRep+ , dearmorIfAsciiArmored+ , dearmorIfAsciiArmoredLenient+ , looksLikeAsciiArmor+ , parsePkts+ , parsePktsEither+ , parsePktsWithWireRep+ , wireRepRefFromInput+ )+import Codec.Encryption.OpenPGP.Types+import Data.Conduit.OpenPGP.Keyring+ ( AuthSecretSubkeyRejectionReason (..)+ , AuthSecretSubkeyUID (..)+ , AuthSecretSubkeysAtReport (..)+ , authSecretSubkeyPrimaryUID+ , authSecretSubkeyRejectedReason+ , authSecretSubkeyUIDs+ , authSecretSubkeyValue+ , authSecretSubkeysAt+ , authSecretSubkeysAtReport+ , conduitDropErrorsAndNothings+ , conduitToAuthSecretSubkeysAt+ , conduitToAuthSecretSubkeysAtReport+ , conduitToSomeTKsDroppingEither+ , conduitToSomeTKsEither+ , conduitToTKsEither+ , conduitToTKsWithWireRepEither+ )+import Tests.Common+ ( addTimestampSeconds+ , loadAndDecompressPkts+ , loadUnencryptedRsaSigner+ , loadV6UnencryptedSecretKeyFixtureForProperty+ , readFixtureLazy+ , readFixturePackets+ , runGet+ , setKeyTimestamp+ , signCertificationAt+ , signSubkeyBindingWithRSAExtrasAt+ , signSubkeyRevocationWithRSAAt+ , timestampToUTCTime+ )++utilityTests :: TestTree+utilityTests =+ testGroup+ "Utility function group"+ [ testCase "pubring as packets" (testParsePktsUtil "pubring.gpg")+ , testCase+ "pubring parsePktsEither equals parsePkts on valid input"+ (testParsePktsEitherUtil "pubring.gpg")+ , testCase+ "parsePktsEither reports truncation errors"+ (testParsePktsEitherFailureUtil "pubring.gpg")+ , testCase+ "parseUnknownTKs drops disallowed primary-key signature context (v4)"+ testParseTKsDropsDisallowedPrimaryKeySigContextV4+ , testCase+ "parseUnknownTKs drops disallowed primary-key signature context (v6)"+ testParseTKsDropsDisallowedPrimaryKeySigContextV6+ , testCase+ "parseUnknownTKs accepts allowed primary-key signature context (v6)"+ testParseTKsAcceptsAllowedPrimaryKeySigContextV6+ , testCase "pubring as TKs" (testParseTKsUtil "pubring.gpg")+ , testCase+ "pubring as typed TKs"+ (testParseTKsTypedUtil "pubring.gpg")+ , testCase+ "pubring parseTKsEither preserves typed parse outcomes"+ (testParseTKsEitherUtil "pubring.gpg")+ , testCase+ "typed TKUnknown conduit partitioning"+ (testConduitToTKsTypedUtil "pubring.gpg")+ , testCase+ "typed TK conduit either reports values without silent drops on valid input"+ (testConduitToSomeTKsEitherUtil "pubring.gpg")+ , testCase+ "typed TK dropping conduit either preserves valid typed results"+ (testConduitToSomeTKsDroppingEitherUtil "pubring.gpg")+ , testCase+ "auth-capable secret-subkey conduit filters revoked/expired/auth flags"+ testConduitToAuthSecretSubkeysAtFiltersByValidityAndAuth+ , testCase+ "auth-capable secret-subkey conduit tracks primary UID over time"+ testConduitToAuthSecretSubkeysAtTracksPrimaryUIDOverTime+ , testCase+ "auth-capable secret-subkey report includes typed rejection reasons"+ testAuthSecretSubkeysAtReportIncludesRejectionReasons+ , testCase+ "pkalgoAbbrev handles RFC9580-era pubkey algorithms"+ testPKAlgoAbbrevRFC9580+ , testCase+ "EightOctetKeyId Show/Read roundtrip uses hex form"+ testEightOctetKeyIdReadShowRoundtrip+ , testCase+ "PktWithWireRep Ord uses raw bytes for tie-breakers"+ testPktWithWireRepOrdUsesRawBytes+ , testCase+ "pubring as packets with provenance"+ (testParsePktsWithWireRepUtil "pubring.gpg")+ , testCase+ "pubring as TKs with provenance"+ (testParseTKsWithWireRepUtil "pubring.gpg")+ , testCase+ "wire provenance tracks original ASCII armor"+ testWireRepRefTracksArmorProvenance+ , testCase+ "dearmorIfAsciiArmored rejects multi-block armored inputs"+ testDearmorRejectsMultipleBlocks+ , testCase+ "dearmorIfAsciiArmoredLenient accepts BOM-prefixed armor"+ testDearmorLenientAcceptsBomPrefixedArmor+ , testCase+ "looksLikeAsciiArmor centralizes armor prefix detection"+ testLooksLikeAsciiArmor+ , testCase+ "wireRepRefFromInput surfaces malformed armored decode errors"+ testWireRepRefRejectsMalformedArmoredInput+ , testCase+ "tksFromWireRep matches any source in TKUnknown provenance list"+ testTksFromWireRepMatchesAnySource+ , testCase+ "TKWithWireRep Semigroup preserves structured provenance"+ testSemigroupTKWithWireRepPreservesStructuredRefs+ , testCase+ "canonicalizeTKWithWireRep matches manual wire-byte ordering"+ testCanonicalizeTKWithWireRepMatchesManualWireOrdering+ , testCase+ "canonicalizeTKWithWireRep reports missing packet refs"+ testCanonicalizeTKWithWireRepReportsMissingRef+ , testCase+ "KeyPkt wrappers preserve packet kind/role round-trips"+ testKeyPktWrappersRoundTrip+ , testCase+ "TK public/secret conversion and projection round-trip"+ testTKTypedRoundTripAndPublicView+ , testCase+ "uat.gpg embeds expected image data from uat.jpg"+ testUatImageFixture+ ]++testPKAlgoAbbrevRFC9580 :: Assertion+testPKAlgoAbbrevRFC9580 = do+ assertEqual+ "X25519 abbreviation"+ "x25"+ (pkalgoAbbrev (toFVal 25))+ assertEqual "X448 abbreviation" "x448" (pkalgoAbbrev (toFVal 26))+ assertEqual+ "Ed25519 abbreviation"+ "e25"+ (pkalgoAbbrev (toFVal 27))+ assertEqual+ "Ed448 abbreviation"+ "e448"+ (pkalgoAbbrev (toFVal 28))++testEightOctetKeyIdReadShowRoundtrip :: Assertion+testEightOctetKeyIdReadShowRoundtrip = do+ let eoki =+ EightOctetKeyId+ (BL.pack [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef])+ assertEqual+ "show prints canonical uppercase hex"+ "0123456789ABCDEF"+ (show eoki)+ assertEqual "read . show roundtrip" eoki (read (show eoki))++testUatImageFixture :: Assertion+testUatImageFixture = do+ expectedImage <- readFixtureLazy "uat.jpg"+ packets <- loadAndDecompressPkts "uat.gpg"+ let images =+ [ imageData+ | UserAttributePkt uas <- packets+ , ImageAttribute _ imageData <- uas+ ]+ assertBool+ "uat.gpg should contain a user-attribute image packet"+ (not (null images))+ assertBool+ "uat.gpg should embed the uat.jpg payload"+ (expectedImage `elem` images)++testParsePktsUtil :: FilePath -> Assertion+testParsePktsUtil fn = do+ cp <- readFixturePackets fn+ pp <- parsePkts `fmap` readFixtureLazy fn+ assertEqual+ "parsePkts utility function gives same results as conduit pipeline"+ cp+ pp++testParsePktsEitherUtil :: FilePath -> Assertion+testParsePktsEitherUtil fn = do+ lbs <- readFixtureLazy fn+ assertEqual+ "parsePktsEither matches parsePkts on valid input"+ (Right (parsePkts lbs))+ (parsePktsEither lbs)++testParsePktsEitherFailureUtil :: FilePath -> Assertion+testParsePktsEitherFailureUtil fn = do+ lbs <- readFixtureLazy fn+ let truncated = BL.take (BL.length lbs - 1) lbs+ case parsePktsEither truncated of+ Left (PktParseError off msg) -> do+ assertBool+ "parsePktsEither reports non-empty parse error messages"+ (not (null msg))+ assertBool+ "parsePktsEither reports an in-range failure offset"+ (off >= 0 && off <= BL.length truncated)+ assertBool+ "legacy parsePkts still returns a parsed prefix on malformed input"+ (length (parsePkts truncated) <= length (parsePkts lbs))+ Right _ ->+ assertFailure+ "parsePktsEither should fail when input is truncated"++testParseTKsUtil :: FilePath -> Assertion+testParseTKsUtil fn = do+ lbs <- readFixtureLazy fn+ cp <-+ DC.runConduitRes $+ CB.sourceLbs lbs+ DC..| conduitGet get+ DC..| conduitToTKsEither+ DC..| conduitDropErrorsAndNothings+ DC..| CL.consume+ let pt = parseUnknownTKs True . parsePkts $ lbs+ assertEqual+ "parsePkts utility function gives same results as conduit pipeline"+ cp+ pt++testParseTKsTypedUtil :: FilePath -> Assertion+testParseTKsTypedUtil fn = do+ lbs <- readFixtureLazy fn+ let packets = parsePkts lbs+ plain = parseUnknownTKs True packets+ typed = parseTKs True packets+ typedPublic = parsePublicTKs True packets+ typedSecret = parseSecretTKs True packets+ assertEqual+ "parseTKs round-trips to the same untyped TKUnknown semantics"+ plain+ (map someTKToUnknown typed)+ assertEqual+ "public + secret typed partitions preserve full typed parse count"+ (length typed)+ (length typedPublic + length typedSecret)++testParseTKsEitherUtil :: FilePath -> Assertion+testParseTKsEitherUtil fn = do+ lbs <- readFixtureLazy fn+ let packets = parsePkts lbs+ typed = parseTKs True packets+ typedEither = parseTKsEither True packets+ assertEqual+ "parseTKsEither right results should match parseTKs"+ typed+ (rights typedEither)+ assertBool+ "parseTKsEither should have no conversion failures for canonical pubring fixture"+ (null (lefts typedEither))++testConduitToTKsTypedUtil :: FilePath -> Assertion+testConduitToTKsTypedUtil fn = do+ lbs <- readFixtureLazy fn+ allTyped <-+ DC.runConduitRes $+ CB.sourceLbs lbs+ DC..| conduitGet get+ DC..| conduitToSomeTKsEither+ DC..| CL.map (join . hush)+ DC..| CL.catMaybes+ DC..| CL.consume+ publicTyped <-+ DC.runConduitRes $+ CB.sourceLbs lbs+ DC..| conduitGet get+ DC..| conduitToSomeTKsEither+ DC..| CL.map (join . hush)+ DC..| CL.map (join . fmap someTKToPublicTK)+ DC..| CL.catMaybes+ DC..| CL.consume+ secretTyped <-+ DC.runConduitRes $+ CB.sourceLbs lbs+ DC..| conduitGet get+ DC..| conduitToSomeTKsEither+ DC..| CL.map (join . hush)+ DC..| CL.map (join . fmap someTKToSecretTK)+ DC..| CL.catMaybes+ DC..| CL.consume+ assertEqual+ "typed conduit round-trips to parseUnknownTKs semantics"+ (parseUnknownTKs True (parsePkts lbs))+ (map someTKToUnknown allTyped)+ assertEqual+ "typed conduit public + secret partitions preserve full count"+ (length allTyped)+ (length publicTyped + length secretTyped)++testConduitToSomeTKsEitherUtil :: FilePath -> Assertion+testConduitToSomeTKsEitherUtil fn = do+ lbs <- readFixtureLazy fn+ results <-+ DC.runConduitRes $+ CB.sourceLbs lbs+ DC..| conduitGet get+ DC..| conduitToSomeTKsEither+ DC..| CL.consume+ let typedFromEither = catMaybes (rights results)+ assertBool+ "conduitToSomeTKsEither should not report failures for canonical pubring fixture"+ (null (lefts results))+ assertEqual+ "conduitToSomeTKsEither right values should match conduitToTKs semantics"+ (parseTKs True (parsePkts lbs))+ typedFromEither++testConduitToSomeTKsDroppingEitherUtil :: FilePath -> Assertion+testConduitToSomeTKsDroppingEitherUtil fn = do+ lbs <- readFixtureLazy fn+ results <-+ DC.runConduitRes $+ CB.sourceLbs lbs+ DC..| conduitGet get+ DC..| conduitToSomeTKsDroppingEither+ DC..| CL.consume+ let typedFromEither = catMaybes (rights results)+ assertBool+ "conduitToSomeTKsDroppingEither should not report failures for canonical pubring fixture"+ (null (lefts results))+ assertEqual+ "conduitToSomeTKsDroppingEither right values should match tolerant parseTKs semantics"+ (parseTKs False (parsePkts lbs))+ typedFromEither++testConduitToAuthSecretSubkeysAtFiltersByValidityAndAuth+ :: Assertion+testConduitToAuthSecretSubkeysAtFiltersByValidityAndAuth = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let baseTime = _timestamp signer+ beforeTime = timestampToUTCTime (addTimestampSeconds baseTime 18)+ afterTime = timestampToUTCTime (addTimestampSeconds baseTime 24)+ uidText = "auth-subkeys@example.org"+ uid = UserId uidText+ secretAddendum = SUUnencrypted (RSAPrivateKey (RSA_PrivateKey signingKey)) 0+ authSubkey = setKeyTimestamp (addTimestampSeconds baseTime 1) signer+ expiringAuthSubkey = setKeyTimestamp (addTimestampSeconds baseTime 2) signer+ signingOnlySubkey = setKeyTimestamp (addTimestampSeconds baseTime 3) signer++ uidCertification <-+ signCertificationAt+ signer+ signingKey+ uid+ (addTimestampSeconds baseTime 8)+ [SigSubPacket False (PrimaryUserId True)]+ authBinding <-+ signSubkeyBindingWithRSAExtrasAt+ signer+ authSubkey+ signingKey+ (addTimestampSeconds baseTime 10)+ [SigSubPacket False (KeyFlags (Set.fromList [AuthKey]))]+ expiringAuthBinding <-+ signSubkeyBindingWithRSAExtrasAt+ signer+ expiringAuthSubkey+ signingKey+ (addTimestampSeconds baseTime 10)+ [ SigSubPacket False (KeyFlags (Set.fromList [AuthKey]))+ , SigSubPacket False (KeyExpirationTime 20)+ ]+ signingOnlyBinding <-+ signSubkeyBindingWithRSAExtrasAt+ signer+ signingOnlySubkey+ signingKey+ (addTimestampSeconds baseTime 10)+ [SigSubPacket False (KeyFlags (Set.fromList [SignDataKey]))]+ authRevocation <-+ signSubkeyRevocationWithRSAAt+ signer+ authSubkey+ signingKey+ (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"++ selectedBefore <-+ DC.runConduitRes $+ CL.sourceList [typedSecret]+ DC..| conduitToAuthSecretSubkeysAt beforeTime+ DC..| CL.consume+ assertEqual+ "pure authSecretSubkeysAt helper and conduit output should match"+ (authSecretSubkeysAt beforeTime typedSecret)+ selectedBefore+ assertEqual+ "before revocation/expiry, conduit should keep only auth-capable secret subkeys"+ 2+ (length selectedBefore)+ let selectedBeforeFps =+ Set.fromList+ ( map+ (fingerprint . keyPktPKPayload . authSecretSubkeyValue)+ selectedBefore+ )+ assertEqual+ "selected auth-capable subkeys should match expected fingerprints"+ ( Set.fromList+ [fingerprint authSubkey, fingerprint expiringAuthSubkey]+ )+ selectedBeforeFps+ mapM_+ ( \selection -> do+ assertEqual+ "selected auth subkey should retain active primary UID context"+ (Just uidText)+ (authSecretSubkeyPrimaryUID selection)+ assertEqual+ "selected auth subkey should expose UID context with primary marker"+ [AuthSecretSubkeyUID uidText True]+ (authSecretSubkeyUIDs selection)+ )+ selectedBefore++ selectedAfter <-+ DC.runConduitRes $+ CL.sourceList [typedSecret]+ DC..| conduitToAuthSecretSubkeysAt afterTime+ DC..| CL.consume+ assertEqual+ "after revocation/expiry, conduit should drop revoked/expired auth subkeys"+ []+ selectedAfter++testConduitToAuthSecretSubkeysAtTracksPrimaryUIDOverTime+ :: Assertion+testConduitToAuthSecretSubkeysAtTracksPrimaryUIDOverTime = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let baseTime = _timestamp signer+ beforeTime = timestampToUTCTime (addTimestampSeconds baseTime 15)+ afterTime = timestampToUTCTime (addTimestampSeconds baseTime 25)+ uidA = UserId "uid-a@example.org"+ UserId uidAText = uidA+ uidB = UserId "uid-b@example.org"+ UserId uidBText = uidB+ secretAddendum = SUUnencrypted (RSAPrivateKey (RSA_PrivateKey signingKey)) 0+ authSubkey = setKeyTimestamp (addTimestampSeconds baseTime 1) signer++ uidACert <-+ signCertificationAt+ signer+ signingKey+ uidA+ (addTimestampSeconds baseTime 10)+ [SigSubPacket False (PrimaryUserId True)]+ uidBCert <-+ signCertificationAt+ signer+ signingKey+ uidB+ (addTimestampSeconds baseTime 20)+ [SigSubPacket False (PrimaryUserId True)]+ authBinding <-+ signSubkeyBindingWithRSAExtrasAt+ signer+ authSubkey+ signingKey+ (addTimestampSeconds baseTime 11)+ [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"++ beforeSelections <-+ DC.runConduitRes $+ CL.sourceList [typedSecret]+ DC..| conduitToAuthSecretSubkeysAt beforeTime+ DC..| CL.consume+ beforeSelection <-+ case beforeSelections of+ [selection] -> pure selection+ other ->+ assertFailure+ ( "expected one auth-capable subkey selection before primary-uid rollover, got "+ ++ show (length other)+ )+ >> fail "unreachable"+ assertEqual+ "earlier primary UID should be selected before newer self-certification"+ (Just uidAText)+ (authSecretSubkeyPrimaryUID beforeSelection)+ assertEqual+ "UID context should include only active UIDs before uid-b certification exists"+ [(uidAText, True)]+ ( map+ ( \u -> (authSecretSubkeyUIDValue u, authSecretSubkeyUIDIsPrimary u)+ )+ (authSecretSubkeyUIDs beforeSelection)+ )++ afterSelections <-+ DC.runConduitRes $+ CL.sourceList [typedSecret]+ DC..| conduitToAuthSecretSubkeysAt afterTime+ DC..| CL.consume+ afterSelection <-+ case afterSelections of+ [selection] -> pure selection+ other ->+ assertFailure+ ( "expected one auth-capable subkey selection after primary-uid rollover, got "+ ++ show (length other)+ )+ >> fail "unreachable"+ assertEqual+ "newer primary UID self-certification should win after rollover"+ (Just uidBText)+ (authSecretSubkeyPrimaryUID afterSelection)+ assertEqual+ "UID context should mark uid-b as primary after rollover"+ [(uidAText, False), (uidBText, True)]+ ( map+ ( \u -> (authSecretSubkeyUIDValue u, authSecretSubkeyUIDIsPrimary u)+ )+ (authSecretSubkeyUIDs afterSelection)+ )++testAuthSecretSubkeysAtReportIncludesRejectionReasons+ :: Assertion+testAuthSecretSubkeysAtReportIncludesRejectionReasons = do+ (signer, signingKey) <- loadUnencryptedRsaSigner+ let baseTime = _timestamp signer+ beforeTime = timestampToUTCTime (addTimestampSeconds baseTime 18)+ afterTime = timestampToUTCTime (addTimestampSeconds baseTime 24)+ uidText = "auth-subkeys@example.org"+ uid = UserId uidText+ secretAddendum = SUUnencrypted (RSAPrivateKey (RSA_PrivateKey signingKey)) 0+ authSubkey = setKeyTimestamp (addTimestampSeconds baseTime 1) signer+ expiringAuthSubkey = setKeyTimestamp (addTimestampSeconds baseTime 2) signer+ signingOnlySubkey = setKeyTimestamp (addTimestampSeconds baseTime 3) signer++ uidCertification <-+ signCertificationAt+ signer+ signingKey+ uid+ (addTimestampSeconds baseTime 8)+ [SigSubPacket False (PrimaryUserId True)]+ authBinding <-+ signSubkeyBindingWithRSAExtrasAt+ signer+ authSubkey+ signingKey+ (addTimestampSeconds baseTime 10)+ [SigSubPacket False (KeyFlags (Set.fromList [AuthKey]))]+ expiringAuthBinding <-+ signSubkeyBindingWithRSAExtrasAt+ signer+ expiringAuthSubkey+ signingKey+ (addTimestampSeconds baseTime 10)+ [ SigSubPacket False (KeyFlags (Set.fromList [AuthKey]))+ , SigSubPacket False (KeyExpirationTime 20)+ ]+ signingOnlyBinding <-+ signSubkeyBindingWithRSAExtrasAt+ signer+ signingOnlySubkey+ signingKey+ (addTimestampSeconds baseTime 10)+ [SigSubPacket False (KeyFlags (Set.fromList [SignDataKey]))]+ authRevocation <-+ signSubkeyRevocationWithRSAAt+ signer+ authSubkey+ signingKey+ (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"++ let beforeReport = authSecretSubkeysAtReport beforeTime typedSecret+ assertEqual+ "report accepted list should match authSecretSubkeysAt before revocation/expiry"+ (authSecretSubkeysAt beforeTime typedSecret)+ (authSecretSubkeysAccepted beforeReport)+ assertEqual+ "before revocation/expiry, only signing-only subkey should be rejected"+ [AuthSecretSubkeyMissingAuthCapability]+ ( map+ authSecretSubkeyRejectedReason+ (authSecretSubkeysRejected beforeReport)+ )++ afterReportsViaConduit <-+ DC.runConduitRes $+ CL.sourceList [typedSecret]+ DC..| conduitToAuthSecretSubkeysAtReport afterTime+ DC..| CL.consume+ afterReport <-+ case afterReportsViaConduit of+ [singleReport] -> pure singleReport+ other ->+ assertFailure+ ( "expected one report from conduitToAuthSecretSubkeysAtReport, got "+ ++ show (length other)+ )+ >> fail "unreachable"+ assertEqual+ "after revocation/expiry, no accepted auth subkeys should remain"+ []+ (authSecretSubkeysAccepted afterReport)+ assertEqual+ "after revocation/expiry, rejected reasons should report invalid-time and missing-auth cases for still-visible candidates"+ [ AuthSecretSubkeySubkeyInvalidAtTime+ , AuthSecretSubkeyMissingAuthCapability+ ]+ ( map+ authSecretSubkeyRejectedReason+ (authSecretSubkeysRejected afterReport)+ )++testPktWithWireRepOrdUsesRawBytes :: Assertion+testPktWithWireRepOrdUsesRawBytes = do+ let src = wireRepRef "src"+ pktA =+ PktWithWireRep+ src+ (ByteRange 0 1)+ "a"+ 0+ (OtherPacketPkt 42 "a")+ pktB =+ PktWithWireRep+ src+ (ByteRange 1 1)+ "b"+ 1+ (BrokenPacketPkt "broken" 42 "b")+ assertEqual+ "base packet ordering can tie on tag-only fallback"+ EQ+ (compare (_pktValue pktA) (_pktValue pktB))+ assertEqual+ "PktWithWireRep ordering should break ties using packet bytes"+ LT+ (compare pktA pktB)++testParsePktsWithWireRepUtil :: FilePath -> Assertion+testParsePktsWithWireRepUtil fn = do+ let fpath = "tests/data/" ++ fn+ lbs <- BL.readFile fpath+ let WireRepInput+ { wireRepInputRef = src+ , wireRepInputPayload = srcBytes+ } =+ either+ ( const+ WireRepInput+ { wireRepInputRef = wireRepRef lbs+ , wireRepInputPayload = lbs+ }+ )+ id+ (wireRepRefFromInput Nothing lbs)+ parsed = parsePktsWithWireRep src srcBytes+ chunkedInput = chunkStrict 7 (BL.toStrict lbs)+ conduitParsed <-+ DC.runConduitRes $+ CB.sourceLbs lbs+ DC..| conduitParsePktsWithWireRep Nothing+ DC..| CL.consume+ conduitParsedChunked <-+ DC.runConduitRes $+ CL.sourceList chunkedInput+ DC..| conduitParsePktsWithWireRep Nothing+ DC..| CL.consume+ assertEqual+ "provenance-aware parsePkts preserves packet semantics"+ (parsePkts lbs)+ (map _pktValue parsed)+ assertEqual+ "conduit and pure provenance-aware packet parsing agree"+ parsed+ conduitParsed+ assertEqual+ "conduit parser handles packets split across chunk boundaries"+ parsed+ conduitParsedChunked+ assertEqual+ "packetsFromWireRep returns all packets for the originating bytestream"+ parsed+ (packetsFromWireRep src parsed)+ mapM_ (assertPktProvenance src srcBytes) parsed+ where+ chunkStrict n bs+ | B.null bs = []+ | otherwise =+ let (prefix, suffix) = B.splitAt n bs+ in prefix : chunkStrict n suffix++testParseTKsWithWireRepUtil :: FilePath -> Assertion+testParseTKsWithWireRepUtil fn = do+ let fpath = "tests/data/" ++ fn+ lbs <- BL.readFile fpath+ let WireRepInput+ { wireRepInputRef = src+ , wireRepInputPayload = srcBytes+ } =+ either+ ( const+ WireRepInput+ { wireRepInputRef = wireRepRef lbs+ , wireRepInputPayload = lbs+ }+ )+ id+ (wireRepRefFromInput Nothing lbs)+ packets = parsePktsWithWireRep src srcBytes+ parsed = parseTKsWithWireRep True packets+ plain = parseUnknownTKs True (map _pktValue packets)+ conduitParsed <-+ DC.runConduitRes $+ CL.sourceList packets+ DC..| conduitToTKsWithWireRepEither+ DC..| CL.map (join . hush)+ DC..| CL.catMaybes+ DC..| CL.consume+ assertEqual+ "provenance-aware parseUnknownTKs preserves TKUnknown semantics"+ plain+ (map _tkValue parsed)+ assertEqual+ "conduit and pure provenance-aware TKUnknown parsing agree"+ parsed+ conduitParsed+ assertEqual+ "tksFromWireRep returns all TKs for the originating bytestream"+ parsed+ (tksFromWireRep src parsed)+ mapM_ (assertTKProvenance src parsed) parsed++assertPktProvenance+ :: WireRepRef -> BL.ByteString -> PktWithWireRep -> Assertion+assertPktProvenance src srcBytes pkt = do+ assertEqual+ "packet raw bytes round-trip back to the same packet"+ (Right (_pktValue pkt))+ (runGet (get :: Get Pkt) (_pktRaw pkt))+ assertEqual+ "packet source reference is preserved"+ src+ (wireRepOfPkt pkt)+ let ByteRange offset len = _pktRange pkt+ assertEqual+ "packet raw bytes match the source bytestream slice"+ (_pktRaw pkt)+ (BL.take len (BL.drop offset srcBytes))++assertTKProvenance+ :: WireRepRef -> [TKWithWireRep] -> TKWithWireRep -> Assertion+assertTKProvenance src allTks tk = do+ assertBool+ "TKUnknown source reference list includes originating source"+ (src `elem` _tkWireRepRefs tk)+ assertEqual+ "TKUnknown source reference is preserved"+ src+ (wireRepOfTK tk)+ assertEqual+ "TKUnknown packet references reconstruct the semantic TKUnknown packet sequence"+ (flattenTK (_tkValue tk))+ (map _pktValue (packetRefsOfTK tk))+ assertEqual+ "TKUnknown 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"+ (tk `elem` tksContainingPacket pkt allTks)+ )+ (packetRefsOfTK tk)+ case toStructuredTKWithWireRep tk of+ Left err ->+ assertFailure ("toStructuredTKWithWireRep failed: " ++ err)+ Right structured -> do+ assertEqual+ "structured provenance retains semantic primary key"+ (_tkuKey (_tkValue tk))+ (_tkStructuredPrimaryKey structured)+ assertEqual+ "structured provenance retains packet source reference list"+ (_tkWireRepRefs tk)+ (_tkStructuredWireRepRefs structured)+ resolved <- resolveStructuredPacketRefs structured+ assertEqual+ "structured provenance resolves packet refs in TKUnknown packet order"+ (map _pktValue (packetRefsOfTK tk))+ (map _pktValue resolved)+ assertEqual+ "structured provenance keeps stable packet ref ids"+ (map packetRefIdOf (packetRefsOfTK tk))+ (map packetRefIdOf resolved)++resolveStructuredPacketRefs+ :: TKStructuredWithWireRep -> IO [PktWithWireRep]+resolveStructuredPacketRefs structured = do+ let collectSigRefs = map _signatureWithWireRefRef+ uidRefs =+ concatMap+ ( \uid ->+ _uidWithWireRefsRef uid+ : collectSigRefs (_uidWithWireRefsSignatures uid)+ )+ (_tkStructuredUIDs structured)+ uatRefs =+ concatMap+ ( \uat ->+ _uatWithWireRefsRef uat+ : collectSigRefs (_uatWithWireRefsSignatures uat)+ )+ (_tkStructuredUAts structured)+ subRefs =+ concatMap+ ( \sub ->+ _subkeyWithWireRefsRef sub+ : collectSigRefs (_subkeyWithWireRefsSignatures sub)+ )+ (_tkStructuredSubkeys structured)+ refIds =+ _tkStructuredPrimaryKeyRef structured+ : collectSigRefs (_tkStructuredDirectSignatures structured)+ ++ uidRefs+ ++ uatRefs+ ++ subRefs+ mapM+ ( \refId ->+ case lookupPacketRef structured refId of+ Nothing ->+ assertFailure+ ("lookupPacketRef failed for ref id " ++ show refId)+ Just pkt -> pure pkt+ )+ refIds++testCanonicalizeTKWithWireRepMatchesManualWireOrdering+ :: Assertion+testCanonicalizeTKWithWireRepMatchesManualWireOrdering = do+ lbs <- readFixtureLazy "pubring.gpg"+ let src = wireRepRef lbs+ parsed = parseTKsWithWireRep True (parsePktsWithWireRep src lbs)+ case parsed of+ (tk : _) ->+ case toStructuredTKWithWireRep tk of+ Left err ->+ assertFailure ("toStructuredTKWithWireRep failed: " ++ err)+ Right structured -> do+ let shuffled =+ structured+ { _tkStructuredDirectSignatures =+ reverse (_tkStructuredDirectSignatures structured)+ , _tkStructuredUIDs =+ reverse+ ( map+ ( \uid ->+ uid+ { _uidWithWireRefsSignatures =+ reverse (_uidWithWireRefsSignatures uid)+ }+ )+ (_tkStructuredUIDs structured)+ )+ , _tkStructuredUAts =+ reverse+ ( map+ ( \uat ->+ uat+ { _uatWithWireRefsSignatures =+ reverse (_uatWithWireRefsSignatures uat)+ }+ )+ (_tkStructuredUAts structured)+ )+ , _tkStructuredSubkeys =+ reverse+ ( map+ ( \sub ->+ sub+ { _subkeyWithWireRefsSignatures =+ reverse (_subkeyWithWireRefsSignatures sub)+ }+ )+ (_tkStructuredSubkeys structured)+ )+ }+ expected <-+ case manualCanonicalizeStructured shuffled of+ Left err ->+ assertFailure ("manual canonicalization failed: " ++ err)+ >> fail "manual canonicalization failed"+ Right x -> pure x+ got <-+ case canonicalizeTKStructuredWithWireRep shuffled of+ Left err ->+ assertFailure+ ("canonicalizeTKStructuredWithWireRep failed: " ++ show err)+ >> fail "canonicalizeTKStructuredWithWireRep failed"+ Right x -> pure x+ assertEqual+ "canonicalizeTKStructuredWithWireRep matches manual wire-byte ordering"+ expected+ got+ wrapped <-+ case canonicalizeTKWithWireRep tk of+ Left err ->+ assertFailure ("canonicalizeTKWithWireRep failed: " ++ show err)+ >> fail "canonicalizeTKWithWireRep failed"+ Right x -> pure x+ structuredCanonical <-+ case canonicalizeTKStructuredWithWireRep structured of+ Left err ->+ assertFailure+ ("canonicalizeTKStructuredWithWireRep failed: " ++ show err)+ >> fail "canonicalizeTKStructuredWithWireRep failed"+ Right x -> pure x+ assertEqual+ "canonicalizeTKWithWireRep delegates to structured canonicalization"+ structuredCanonical+ wrapped+ [] ->+ assertFailure+ "pubring.gpg should parse to at least one provenance-aware TKUnknown"+ where+ manualCanonicalizeStructured+ :: TKStructuredWithWireRep -> Either String TKUnknown+ manualCanonicalizeStructured structured = do+ direct <- sortSigs (_tkStructuredDirectSignatures structured)+ uids <-+ sortByRef _uidWithWireRefsRef+ =<< mapM+ ( \uid -> do+ sigs <- sortSigs (_uidWithWireRefsSignatures uid)+ Right (uid, sigs)+ )+ (_tkStructuredUIDs structured)+ uats <-+ sortByRef _uatWithWireRefsRef+ =<< mapM+ ( \uat -> do+ sigs <- sortSigs (_uatWithWireRefsSignatures uat)+ Right (uat, sigs)+ )+ (_tkStructuredUAts structured)+ subs <-+ sortByRef _subkeyWithWireRefsRef+ =<< mapM+ ( \sub -> do+ sigs <- sortSigs (_subkeyWithWireRefsSignatures sub)+ Right (sub, sigs)+ )+ (_tkStructuredSubkeys structured)+ Right $+ TKUnknown+ { _tkuKey = _tkStructuredPrimaryKey structured+ , _tkuRevs = map _signatureWithWireRefValue direct+ , _tkuUIDs =+ map+ ( \(uid, sigs) ->+ (_uidWithWireRefsValue uid, map _signatureWithWireRefValue sigs)+ )+ uids+ , _tkuUAts =+ map+ ( \(uat, sigs) ->+ (_uatWithWireRefsValue uat, map _signatureWithWireRefValue sigs)+ )+ uats+ , _tkuSubs =+ map+ ( \(sub, sigs) ->+ ( _subkeyWithWireRefsValue sub+ , map _signatureWithWireRefValue sigs+ )+ )+ subs+ }+ where+ wireBytes refId =+ maybe+ (Left ("lookupPacketRef failed for ref id " ++ show refId))+ (Right . _pktRaw)+ (lookupPacketRef structured refId)++ sortSigs sigs = do+ keyed <-+ mapM+ ( \sig -> do+ raw <- wireBytes (_signatureWithWireRefRef sig)+ Right ((raw, _signatureWithWireRefRef sig), sig)+ )+ sigs+ Right (map snd (sortOn fst keyed))++ sortByRef refAccessor items = do+ keyed <-+ mapM+ ( \(x, sigs) -> do+ raw <- wireBytes (refAccessor x)+ Right ((raw, refAccessor x), (x, sigs))+ )+ items+ Right (map snd (sortOn fst keyed))++testCanonicalizeTKWithWireRepReportsMissingRef :: Assertion+testCanonicalizeTKWithWireRepReportsMissingRef = do+ lbs <- readFixtureLazy "pubring.gpg"+ let src = wireRepRef lbs+ parsed = parseTKsWithWireRep True (parsePktsWithWireRep src lbs)+ case parsed of+ (tk : _) ->+ case toStructuredTKWithWireRep tk of+ Left err ->+ assertFailure ("toStructuredTKWithWireRep failed: " ++ err)+ Right structured -> do+ let badRef = PacketRefId src 999999+ brokenWithBadRef =+ case _tkStructuredDirectSignatures structured of+ (sig : rest) ->+ Just+ structured+ { _tkStructuredDirectSignatures =+ sig {_signatureWithWireRefRef = badRef} : rest+ }+ [] ->+ case _tkStructuredUIDs structured of+ (uid : restUIDs) ->+ Just+ structured+ { _tkStructuredUIDs =+ uid {_uidWithWireRefsRef = badRef} : restUIDs+ }+ [] ->+ case _tkStructuredUAts structured of+ (uat : restUATs) ->+ Just+ structured+ { _tkStructuredUAts =+ uat {_uatWithWireRefsRef = badRef} : restUATs+ }+ [] ->+ case _tkStructuredSubkeys structured of+ (sub : restSubs) ->+ Just+ structured+ { _tkStructuredSubkeys =+ sub {_subkeyWithWireRefsRef = badRef} : restSubs+ }+ [] -> Nothing+ case brokenWithBadRef of+ Nothing ->+ assertFailure+ "pubring.gpg first TKUnknown unexpectedly has no direct signatures, UIDs, UATs, or subkeys"+ Just broken ->+ case canonicalizeTKStructuredWithWireRep broken of+ Left (CanonicalizeMissingPacketRef ref) ->+ assertEqual+ "missing ref error should include unresolved ref id"+ badRef+ ref+ Left err ->+ assertFailure+ ("Expected CanonicalizeMissingPacketRef, got " ++ show err)+ Right _ ->+ assertFailure+ "Expected canonicalization to fail on missing packet ref"+ [] ->+ assertFailure+ "pubring.gpg should parse to at least one provenance-aware TKUnknown"++testWireRepRefTracksArmorProvenance :: Assertion+testWireRepRefTracksArmorProvenance = do+ armored <- readFixtureLazy "v6-secret.pgp.aa"+ case wireRepRefFromInput Nothing armored of+ Left err ->+ assertFailure+ ("wireRepRefFromInput failed on armored input: " ++ err)+ Right+ WireRepInput+ { wireRepInputRef = src+ , wireRepInputPayload = payload+ } -> do+ assertBool+ "wireRepRefFromInput should mark ASCII-armored input as originally armored"+ (_wireRepWasOriginallyArmored src)+ assertBool+ "dearmored payload should parse into packets"+ (not (null (parsePktsWithWireRep src payload)))++testDearmorRejectsMultipleBlocks :: Assertion+testDearmorRejectsMultipleBlocks = do+ armored <- readFixtureLazy "v6-secret.pgp.aa"+ case dearmorIfAsciiArmored (armored <> "\n" <> armored) of+ Left err ->+ assertBool+ "multi-block rejection error should mention expected single block"+ ("expected exactly one" `isInfixOf` err)+ Right _ ->+ assertFailure+ "dearmorIfAsciiArmored unexpectedly accepted multi-block armor input"++testDearmorLenientAcceptsBomPrefixedArmor :: Assertion+testDearmorLenientAcceptsBomPrefixedArmor = do+ armored <- readFixtureLazy "v6-secret.pgp.aa"+ let bomPrefixed = BL.pack [0xef, 0xbb, 0xbf] <> armored+ case dearmorIfAsciiArmored bomPrefixed of+ Right (False, _) -> pure ()+ Right (True, _) ->+ assertFailure+ "strict dearmorIfAsciiArmored unexpectedly treated BOM-prefixed input as armored"+ Left err ->+ assertFailure+ ("strict dearmorIfAsciiArmored failed unexpectedly: " ++ err)+ case dearmorIfAsciiArmoredLenient bomPrefixed of+ Left err ->+ assertFailure+ ("lenient dearmor should decode BOM-prefixed armor: " ++ err)+ Right (wasArmored, payload) -> do+ assertBool+ "lenient dearmor should report armored input"+ wasArmored+ assertBool+ "lenient dearmor payload should parse as packets"+ (not (null (parsePkts payload)))++testLooksLikeAsciiArmor :: Assertion+testLooksLikeAsciiArmor = do+ let armoredPrefix = "\n\t -----BEGIN PGP MESSAGE-----\nYWJj\n"+ partialPrefix = "-----BEGIN PG"+ binaryPrefix = BL.pack [0x99, 0x01, 0x02, 0x03]+ assertBool+ "looksLikeAsciiArmor accepts canonical armored headers with leading whitespace"+ (looksLikeAsciiArmor armoredPrefix)+ assertBool+ "looksLikeAsciiArmor rejects partial armored headers"+ (not (looksLikeAsciiArmor partialPrefix))+ assertBool+ "looksLikeAsciiArmor rejects binary packet prefixes"+ (not (looksLikeAsciiArmor binaryPrefix))++testWireRepRefRejectsMalformedArmoredInput :: Assertion+testWireRepRefRejectsMalformedArmoredInput = do+ let malformed =+ "-----BEGIN PGP MESSAGE-----\n"+ <> "not base64 and no checksum\n"+ <> "-----END PGP MESSAGE-----\n"+ case wireRepRefFromInput Nothing malformed of+ Left _ -> pure ()+ Right _ ->+ assertFailure+ "wireRepRefFromInput unexpectedly accepted malformed ASCII-armored input"++testTksFromWireRepMatchesAnySource :: Assertion+testTksFromWireRepMatchesAnySource = do+ lbs <- readFixtureLazy "pubring.gpg"+ let srcA = wireRepRef lbs+ srcB = namedWireRepRef "synthetic-merge-source" lbs+ parsed = parseTKsWithWireRep True (parsePktsWithWireRep srcA lbs)+ case parsed of+ (tk : _) -> do+ let multiSourceTk = tk {_tkWireRepRefs = srcA :| [srcB]}+ assertBool+ "tksFromWireRep matches TKs whose source list contains the queried source"+ (multiSourceTk `elem` tksFromWireRep srcA [multiSourceTk])+ assertBool+ "tksFromWireRep can match secondary provenance sources"+ (multiSourceTk `elem` tksFromWireRep srcB [multiSourceTk])+ [] ->+ assertFailure+ "pubring.gpg should parse to at least one provenance-aware TKUnknown"++testSemigroupTKWithWireRepPreservesStructuredRefs :: Assertion+testSemigroupTKWithWireRepPreservesStructuredRefs = do+ lbs <- readFixtureLazy "pubring.gpg"+ let srcA = wireRepRef lbs+ srcB = namedWireRepRef "synthetic-merge-source" lbs+ parsed = parseTKsWithWireRep True (parsePktsWithWireRep srcA lbs)+ case parsed of+ (tk : _) -> do+ let remappedPackets = map (\pkt -> pkt {_pktWireRepRef = srcB}) (packetRefsOfTK tk)+ tkFromSecondSource =+ TKWithWireRep+ (srcB :| [])+ (spanByteRanges (map _pktRange remappedPackets))+ remappedPackets+ (_tkValue tk)+ merged = tk <> tkFromSecondSource+ mergedRefIds = map packetRefIdOf (packetRefsOfTK merged)+ assertEqual+ "Semigroup preserves TKUnknown semantic merge behavior"+ (_tkValue tk <> _tkValue tkFromSecondSource)+ (_tkValue merged)+ assertBool+ "Semigroup merged provenance references include both sources"+ ( srcA `elem` wireRepsOfTK merged+ && srcB `elem` wireRepsOfTK merged+ )+ assertEqual+ "Semigroup result packet refs match merged TKUnknown packet sequence"+ (flattenTK (_tkValue merged))+ (map _pktValue (packetRefsOfTK merged))+ assertEqual+ "Semigroup result keeps packet refs unique by source-aware PacketRefId"+ (length mergedRefIds)+ (length (nub mergedRefIds))+ case toStructuredTKWithWireRep merged of+ Left err ->+ assertFailure+ ("toStructuredTKWithWireRep failed for Semigroup result: " ++ err)+ Right structured -> do+ resolved <- resolveStructuredPacketRefs structured+ assertEqual+ "Semigroup result structured refs resolve in packet order"+ (map _pktValue (packetRefsOfTK merged))+ (map _pktValue resolved)+ [] ->+ assertFailure+ "pubring.gpg should parse to at least one provenance-aware TKUnknown"++testKeyPktWrappersRoundTrip :: Assertion+testKeyPktWrappersRoundTrip = do+ fixture <- loadV6UnencryptedSecretKeyFixtureForProperty+ case fixture of+ Left err ->+ assertFailure err+ Right (pkp, ska, _) -> do+ let publicPrimaryPkt = PublicKeyPkt pkp+ publicSubkeyPkt = PublicSubkeyPkt pkp+ secretPrimaryPkt = SecretKeyPkt pkp ska+ secretSubkeyPkt = SecretSubkeyPkt pkp ska+ assertEqual+ "mkPrimaryKeyPkt preserves public primary packets"+ publicPrimaryPkt+ (someKeyPktToPkt (mkPrimaryKeyPkt pkp Nothing))+ assertEqual+ "mkPrimaryKeyPkt preserves secret primary packets"+ secretPrimaryPkt+ (someKeyPktToPkt (mkPrimaryKeyPkt pkp (Just ska)))+ assertEqual+ "mkSubkeyKeyPkt preserves public subkey packets"+ publicSubkeyPkt+ (someKeyPktToPkt (mkSubkeyKeyPkt pkp Nothing))+ assertEqual+ "mkSubkeyKeyPkt preserves secret subkey packets"+ secretSubkeyPkt+ (someKeyPktToPkt (mkSubkeyKeyPkt pkp (Just ska)))+ case pktToPublicKeyPkt publicPrimaryPkt of+ Nothing ->+ assertFailure "pktToPublicKeyPkt should accept PublicKeyPkt"+ Just keyPkt -> do+ assertEqual+ "public primary role is preserved"+ KeyPktPrimary+ (keyPktRole keyPkt)+ assertEqual+ "public primary TKUnknown key view is preserved"+ (pkp, Nothing)+ (keyPktTKKey keyPkt)+ assertEqual+ "public primary round-trips through KeyPkt"+ publicPrimaryPkt+ (keyPktToPkt keyPkt)+ case pktToPublicKeyPkt publicSubkeyPkt of+ Nothing ->+ assertFailure "pktToPublicKeyPkt should accept PublicSubkeyPkt"+ Just keyPkt ->+ assertEqual+ "public subkey role is preserved"+ KeyPktSubkey+ (keyPktRole keyPkt)+ case pktToSecretKeyPkt secretPrimaryPkt of+ Nothing ->+ assertFailure "pktToSecretKeyPkt should accept SecretKeyPkt"+ Just keyPkt -> do+ assertEqual+ "secret primary role is preserved"+ KeyPktPrimary+ (keyPktRole keyPkt)+ assertEqual+ "secret primary TKUnknown key view is preserved"+ (pkp, Just ska)+ (keyPktTKKey keyPkt)+ assertEqual+ "secret primary round-trips through KeyPkt"+ secretPrimaryPkt+ (keyPktToPkt keyPkt)+ assertEqual+ "secret primary public view downgrades to PublicKeyPkt"+ publicPrimaryPkt+ (keyPktToPkt (keyPktToPublicView keyPkt))+ case pktToSecretKeyPkt secretSubkeyPkt of+ Nothing ->+ assertFailure "pktToSecretKeyPkt should accept SecretSubkeyPkt"+ Just keyPkt -> do+ assertEqual+ "secret subkey role is preserved"+ KeyPktSubkey+ (keyPktRole keyPkt)+ assertEqual+ "secret subkey public view downgrades to PublicSubkeyPkt"+ publicSubkeyPkt+ (keyPktToPkt (keyPktToPublicView keyPkt))+ case pktToSomeKeyPktEither (UserIdPkt "not a key packet") of+ Left (NotAKeyPacket pkt) ->+ assertEqual+ "non-key coercion error reports the original packet"+ (UserIdPkt "not a key packet")+ pkt+ other ->+ assertFailure+ ("Expected NotAKeyPacket error, got " ++ show other)++testTKTypedRoundTripAndPublicView :: Assertion+testTKTypedRoundTripAndPublicView = do+ pubringBytes <- readFixtureLazy "pubring.gpg"+ let publicParsed = parseUnknownTKs 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"+ >> fail "unreachable"+ assertEqual+ "public typed TKUnknown round-trips back to untyped TKUnknown"+ publicTk+ (someTKToUnknown publicTyped)++ armored <- readFixtureLazy "v6-secret.pgp.aa"+ secretTk <- do+ payload <-+ case dearmorIfAsciiArmored armored of+ Left err ->+ assertFailure ("failed to decode v6-secret fixture: " ++ err)+ >> fail "unreachable"+ Right (_, bs) -> pure bs+ case parseUnknownTKs 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"+ >> 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)+ }+ assertEqual+ "publicViewTK drops secret material from primary/subkeys"+ expectedPublic+ projectedPublic++flattenTK :: TKUnknown -> [Pkt]+flattenTK tk =+ [someKeyPktToPkt (mkPrimaryKeyPkt pkp mska)]+ ++ map SignaturePkt (_tkuRevs 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++testParseTKsDropsDisallowedPrimaryKeySigContextV4 :: Assertion+testParseTKsDropsDisallowedPrimaryKeySigContextV4 = do+ let pkp =+ PKPayload+ V4+ (ThirtyTwoBitTimeStamp 0)+ 0+ EdDSA+ ( EdDSAPubKey+ EdSigningCurve25519+ ( PrefixedNativeEPoint+ (EPoint (os2ip (B.cons 0x40 (B.replicate 32 0x01))))+ )+ )+ invalidSig = SigV4 GenericCert RSA SHA512 [] [] 0 (MPI 0 :| [])+ case parseUnknownTKs True [PublicKeyPkt pkp, SignaturePkt invalidSig] of+ [tk] ->+ assertEqual+ "parseUnknownTKs True should drop GenericCert as a primary-key signature in v4"+ []+ (_tkuRevs tk)+ other ->+ assertFailure+ ( "Expected one TKUnknown when dropping invalid v4 signature context, got "+ ++ show other+ )++testParseTKsDropsDisallowedPrimaryKeySigContextV6 :: Assertion+testParseTKsDropsDisallowedPrimaryKeySigContextV6 = do+ let pkp =+ PKPayload+ V6+ (ThirtyTwoBitTimeStamp 0)+ 0+ EdDSA+ (EdDSAPubKey EdSigningCurve25519 (NativeEPoint (EPoint 1)))+ invalidSig =+ SigV6+ GenericCert+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x01))+ []+ []+ 0+ (MPI 0 :| [])+ case parseUnknownTKs True [PublicKeyPkt pkp, SignaturePkt invalidSig] of+ [tk] ->+ assertEqual+ "parseUnknownTKs True should drop GenericCert as a primary-key signature in v6"+ []+ (_tkuRevs tk)+ other ->+ assertFailure+ ( "Expected one TKUnknown when dropping invalid v6 signature context, got "+ ++ show other+ )++testParseTKsAcceptsAllowedPrimaryKeySigContextV6 :: Assertion+testParseTKsAcceptsAllowedPrimaryKeySigContextV6 = do+ let pkp =+ PKPayload+ V6+ (ThirtyTwoBitTimeStamp 0)+ 0+ EdDSA+ (EdDSAPubKey EdSigningCurve25519 (NativeEPoint (EPoint 1)))+ allowedSig =+ SigV6+ KeyRevocationSig+ EdDSA+ SHA512+ (SignatureSalt (BL.replicate 32 0x02))+ []+ []+ 0+ (MPI 0 :| [])+ case parseUnknownTKs True [PublicKeyPkt pkp, SignaturePkt allowedSig] of+ [tk] ->+ assertBool+ "parseUnknownTKs True should keep allowed v6 key-revocation signatures on primary keys"+ (not (null (_tkuRevs tk)))+ other ->+ assertFailure+ ( "Expected one TKUnknown with a retained v6 revocation signature, got "+ ++ show other+ )