hOpenPGP-3.0.0: tests/Tests/Common.hs
-- Common.hs: hOpenPGP test suite
-- Copyright © 2012-2026 Clint Adams
-- This software is released under the terms of the Expat license.
-- (See the LICENSE file).
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE TypeApplications #-}
module Tests.Common
( addTimestampSeconds
, armorPayload
, assertFalse
, assertTrue
, collectSecretKeyInfos
, conduitDecryptWithPKESKContext
, loadArmor
, loadAndDecompressPkts
, loadSEIPDv2FixtureWithV4Secret
, loadUnencryptedRsaSigner
, loadV4EncryptedSecretKeyFixtureForProperty
, loadV6UnencryptedSecretKeyFixtureForProperty
, prependUnusableLatestPKESK
, readFixtureLazy
, readFixturePackets
, readFixturePayload
, reorderPrecedingPKESKs
, reverseIf
, runGet -- FIXME: this is confusing
, selectRecipientKeyInfo
, setKeyTimestamp
, signCertificationAt
, signSubkeyBindingWithRSAExtrasAt
, signSubkeyRevocationWithRSAAt
, timestampToUTCTime
, assertSingleFailureContainsTimeline
, assertSingleSignerFingerprint
, encryptMessageDefault
, expectV4PKPayload
, expectV6PKPayload
, extractV4SignatureAlgorithmFields
, fp
, loadKeyring
, loadDeterministicEd25519Signer
, loadDeterministicEd25519SignerV6
, loadDeterministicEd448Signer
, loadDeterministicEd448SignerV6
, loadUnencryptedRsaSignerV6
, messageIssuerSubpacketsAt
, mkTestKeyring
, setPKAlgorithm
, signBinaryMessageWithRSAAt
, signBinaryMessageWithEd25519At
, signKeyRevocationWithReasonAt
, signKeyRevocationWithReasonAndExtrasAt
, signSubkeyBindingWithRSAAt
, verifyTimelinePackets
, signCertificationRevocationWithEd25519At
, signCertificationWithEd25519At
, verificationFixtureGroup
, verifyMessageFromBytestring
, verifyMessageFromBytestringBatch
, verifyMessageFromPackets
, verifyMessageFromPacketsBatch
, certificateVerificationFixtures
, fixturePath
, messageVerificationFixtures
, readPKIPassphrase
, setKeyVersion
, signCertificationRevocationAt
, aesKeyWrapRFC3394ForTest
, assertX25519EskShape
, assertX448EskShape
, buildCurve25519LegacyKdfParamForTest
, buildECDHKDFParamForTest
, cgp
, conduitDecrypt -- FIXME: this is confusing
, conduitDecryptChecked
, conduitDecryptCheckedWithDecryptPolicy
, conduitDecryptWithCandidatesCallbackAndPolicy
, conduitDecryptWithDecryptPolicy
, deriveECDHKekForTest
, deriveX25519KekForTest
, deriveX448KekForTest
, doPkeyAndSkeyMatch
, encodeChecksum16
, forceVersionedRecipientIdentifier
, isPrecedingESK
, mkPKESKSessionMaterialOrFail
, readFixtureStrict
, selectRecipientKeyInfoByRawRecipientId
, signDirectKeyWithRSAExtrasAt
, testEncodeOpenPGPSessionMaterial
, testParsedRSASecretKeyPKCS15DecryptNotMessageNotRecognized
, testSEIPDv2ForV4KeyArmor
, testSEIPDv2TwoRecipientsArmor
, testSEIPDv2ThreeRecipientsArmor
)
where
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, testCase)
import Codec.Encryption.OpenPGP.Arbitrary ()
import qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA
import Codec.Encryption.OpenPGP.ASCIIArmor.Types (Armor(..), ArmorType(..))
import Codec.Encryption.OpenPGP.Compression (decompressPkt)
import Codec.Encryption.OpenPGP.Encrypt
( PKESKSessionMaterial
, mkPKESKSessionMaterial
, encodeOpenPGPSessionMaterial
)
import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)
import Codec.Encryption.OpenPGP.Internal
( curveFromCurve
, curveToCurveoidBS
, emptyPSC
, lastPrimaryKey
, lastUIDorUAt
, lastSubkey
)
import Codec.Encryption.OpenPGP.KeySelection (parseFingerprint)
import Codec.Encryption.OpenPGP.Message
( asV4PKPayload
, asV6PKPayload
, ClearPayload
, EncryptedPayload
, encryptMessage
, EncryptMessageOptions(..)
, Passphrase
, MessageError(..)
, RecoveredSessionMaterial(..)
, SessionMaterialExposure(..)
, VersionedPKPayload
)
import Codec.Encryption.OpenPGP.SecretKey
( decryptPrivateKey
)
import Codec.Encryption.OpenPGP.Serialize
( dearmorIfAsciiArmored
, parsePkts
)
import Codec.Encryption.OpenPGP.SerializeForSigs (payloadForSig)
import Codec.Encryption.OpenPGP.Signatures
( VerificationError(..)
, renderVerificationError
, renderSignError
, signCertificationWithRSA
, signCertRevocationWithRSA
, signSubkeyRevocationWithRSA
, signDataWithRSA
, signDataWithEd25519
, signDirectKeyWithRSA
, signKeyRevocationWithRSA
)
import Codec.Encryption.OpenPGP.Types
import Control.Monad (unless, void)
import qualified "crypton" Crypto.Cipher.AES as AES
import qualified "crypton" Crypto.Cipher.Types as CCT
import qualified Crypto.Error as CE
import qualified Crypto.Hash as CH
import qualified Crypto.Hash.Algorithms as CHA
import Crypto.KDF.HKDF (expand, extract)
import qualified Crypto.PubKey.Ed25519 as Ed25519
import qualified Crypto.PubKey.Ed448 as Ed448
import qualified Crypto.PubKey.RSA.PKCS15 as P15
import Control.Monad.Trans.Resource (ResourceT)
import Crypto.Number.Serialize (os2ip)
import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
import qualified Crypto.PubKey.RSA as RSA
import qualified Data.ByteArray as BA
import Data.Bifunctor (bimap, first)
import Data.Binary (get)
import Data.Binary.Get
( Get
, getLazyByteString
, getRemainingLazyByteString
, getWord16be
, getWord8
, runGetOrFail
)
import Data.Binary.Put (putWord64be, runPut)
import Data.Bits (xor)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BLC8
import qualified Data.ByteString.Base16.Lazy as B16L
import Data.Char (toUpper)
import Data.Conduit.OpenPGP.Compression (conduitDecompress)
import Data.Conduit.OpenPGP.Decrypt
( DecryptKeyResolution(..)
, DecryptOptions(..)
, PKESKRecipientKey(..)
, DecryptOutcome(..)
)
import qualified Data.Conduit.OpenPGP.Decrypt as DCD
import Codec.Encryption.OpenPGP.Policy
( DecryptPolicy
, defaultDecryptPolicy
)
import Data.Conduit.OpenPGP.Keyring
( conduitToPublicViewTKs
, sinkPublicKeyringMap
, partitionSomeTKs
)
import Data.Conduit.OpenPGP.Message
( VerificationOptions(..)
, VerificationPolicy(..)
, defaultVerificationOptions
, VerificationMode(..)
, verifyMessage
, verifyMessagePackets
)
import Data.Conduit.Serialization.Binary (conduitGet)
import Data.List (isInfixOf)
import Data.List.NonEmpty (NonEmpty(..))
import Data.Maybe (catMaybes, listToMaybe)
import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Data.Word (Word32, Word64)
import qualified Data.Conduit as DC
import qualified Data.Conduit.Binary as CB
import qualified Data.Conduit.List as CL
import qualified Crypto.PubKey.ECC.Types as ECCT
-- Test assertion helpers
assertTrue :: String -> Bool -> Assertion
assertTrue msg b = assertBool msg b
assertFalse :: String -> Bool -> Assertion
assertFalse msg b = assertBool msg (not b)
fixturePath :: FilePath -> FilePath
fixturePath file = "tests/data/" ++ file
readFixtureLazy :: FilePath -> IO BL.ByteString
readFixtureLazy = BL.readFile . fixturePath
readFixtureStrict :: FilePath -> IO B.ByteString
readFixtureStrict = B.readFile . fixturePath
readFixturePackets :: FilePath -> IO [Pkt]
readFixturePackets file =
DC.runConduitRes $ CB.sourceFile (fixturePath file) DC..| conduitGet get DC..| CL.consume
readFixtureDecompressedPackets :: FilePath -> IO [Pkt]
readFixtureDecompressedPackets file =
DC.runConduitRes $
CB.sourceFile (fixturePath file) DC..| conduitGet get DC..| conduitDecompress DC..| CL.consume
loadFirstArmor :: FilePath -> IO Armor
loadFirstArmor file = do
armors <- loadArmor file
case armors of
(a:_) -> pure a
[] -> assertFailure (file ++ " armor file contained no armor blocks") >> fail "expected armor block"
readPKIPassphrase :: IO BL.ByteString
readPKIPassphrase = readFixtureLazy "pki-password.txt"
-- this needs a better name
runGet :: Get a -> BL.ByteString -> Either String a
runGet g bs = bimap (\(_, _, x) -> x) (\(_, _, x) -> x) (runGetOrFail g bs)
extractV4SignatureAlgorithmFields ::
BL.ByteString -> Either String (PubKeyAlgorithm, B.ByteString)
extractV4SignatureAlgorithmFields =
runGet $ do
version <- getWord8
if version /= 4
then fail ("expected v4 signature payload, got version " ++ show version)
else do
_ <- getWord8 -- sig type
pka <- getWord8
_ <- getWord8 -- hash algo
hlen <- getWord16be
_ <- getLazyByteString (fromIntegral hlen)
ulen <- getWord16be
_ <- getLazyByteString (fromIntegral ulen)
_ <- getWord16be -- left16
algorithmFields <- getRemainingLazyByteString
pure (toFVal pka, BL.toStrict algorithmFields)
conduitDecrypt ::
(String -> IO BL.ByteString)
-> DC.ConduitT Pkt Pkt (ResourceT IO) ()
conduitDecrypt cb =
void $
DCD.conduitDecrypt
DecryptOptions
{ decryptOptionsKeyResolution = DecryptWithoutPKESK
, decryptOptionsPolicy = defaultDecryptPolicy
, decryptOptionsPassphraseCallback = cb
}
conduitDecryptWithPKESKContext ::
(Pkt -> IO (Maybe PKESKRecipientKey))
-> (String -> IO BL.ByteString)
-> DC.ConduitT Pkt Pkt (ResourceT IO) ()
conduitDecryptWithPKESKContext pkcb cb =
void $
DCD.conduitDecrypt
DecryptOptions
{ decryptOptionsKeyResolution = DecryptWithUnwrapCandidatesCallback (asPKESKUnwrapCandidatesCallback pkcb)
, decryptOptionsPolicy = defaultDecryptPolicy
, decryptOptionsPassphraseCallback = cb
}
conduitDecryptWithDecryptPolicy ::
DecryptPolicy
-> (Pkt -> IO (Maybe PKESKRecipientKey))
-> (String -> IO BL.ByteString)
-> DC.ConduitT Pkt Pkt (ResourceT IO) ()
conduitDecryptWithDecryptPolicy dp pkcb cb =
void $
DCD.conduitDecrypt
DecryptOptions
{ decryptOptionsKeyResolution = DecryptWithUnwrapCandidatesCallback (asPKESKUnwrapCandidatesCallback pkcb)
, decryptOptionsPolicy = dp
, decryptOptionsPassphraseCallback = cb
}
conduitDecryptChecked ::
(String -> IO BL.ByteString)
-> DC.ConduitT Pkt Pkt (ResourceT IO) DecryptOutcome
conduitDecryptChecked cb =
DCD.conduitDecrypt
DecryptOptions
{ decryptOptionsKeyResolution = DecryptWithoutPKESK
, decryptOptionsPolicy = defaultDecryptPolicy
, decryptOptionsPassphraseCallback = cb
}
conduitDecryptCheckedWithDecryptPolicy ::
DecryptPolicy
-> (Pkt -> IO (Maybe PKESKRecipientKey))
-> (String -> IO BL.ByteString)
-> DC.ConduitT Pkt Pkt (ResourceT IO) DecryptOutcome
conduitDecryptCheckedWithDecryptPolicy dp pkcb cb =
DCD.conduitDecrypt
DecryptOptions
{ decryptOptionsKeyResolution = DecryptWithUnwrapCandidatesCallback (asPKESKUnwrapCandidatesCallback pkcb)
, decryptOptionsPolicy = dp
, decryptOptionsPassphraseCallback = cb
}
conduitDecryptWithCandidatesCallbackAndPolicy ::
DecryptPolicy
-> (KeyIdentifier -> PubKeyAlgorithm -> IO [PKESKRecipientKey])
-> (String -> IO BL.ByteString)
-> DC.ConduitT Pkt Pkt (ResourceT IO) ()
conduitDecryptWithCandidatesCallbackAndPolicy dp candCb cb =
void $
DCD.conduitDecrypt
DecryptOptions
{ decryptOptionsKeyResolution = DecryptWithUnwrapCandidatesCallback candCb
, decryptOptionsPolicy = dp
, decryptOptionsPassphraseCallback = cb
}
asPKESKUnwrapCandidatesCallback ::
(Pkt -> IO (Maybe PKESKRecipientKey))
-> KeyIdentifier
-> PubKeyAlgorithm
-> IO [PKESKRecipientKey]
asPKESKUnwrapCandidatesCallback pkcb keyIdentifier pka = do
mk <- pkcb (pkeskProbePacket keyIdentifier pka)
pure (maybe [] (: []) mk)
pkeskProbePacket :: KeyIdentifier -> PubKeyAlgorithm -> Pkt
pkeskProbePacket keyIdentifier pka =
case keyIdentifier of
KeyIdentifierWildcard ->
PKESKPkt
(PKESKPayloadV3Packet
(PKESKPayloadV3 3 (EightOctetKeyId (BL.replicate 8 0)) pka (MPI 0 :| [])))
KeyIdentifierEightOctet rid ->
PKESKPkt
(PKESKPayloadV3Packet
(PKESKPayloadV3 3 rid pka (MPI 0 :| [])))
KeyIdentifierFingerprint rid ->
PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 (unFingerprint rid) pka mempty))
readFixturePayload :: FilePath -> IO BL.ByteString
readFixturePayload fpr = do
bs <- BL.readFile ("tests/data/" ++ fpr)
case dearmorIfAsciiArmored bs of
Left err -> assertFailure ("ASCII armor decode failed for " ++ fpr ++ ": " ++ err) >> pure mempty
Right (_, payload) -> pure payload
testParsedRSASecretKeyPKCS15DecryptNotMessageNotRecognized :: Assertion
testParsedRSASecretKeyPKCS15DecryptNotMessageNotRecognized = do
secretPackets <-
DC.runConduitRes $
CB.sourceFile "tests/data/unencrypted.seckey" DC..| conduitGet get DC..| CL.consume
(publicKey, privateKey) <-
case secretPackets of
(SecretKeyPkt pkp ska:_) ->
case (_pubkey pkp, ska) of
(RSAPubKey (RSA_PublicKey pub), SUUnencrypted (RSAPrivateKey (RSA_PrivateKey prv)) _) ->
pure (pub, prv)
_ ->
assertFailure "unencrypted.seckey did not contain a parseable unencrypted RSA key pair" >>
fail "expected RSA key pair from parsed secret key packet"
_ ->
assertFailure "unencrypted.seckey did not begin with a secret key packet" >>
fail "expected secret key packet"
let plaintext = "pkcs1-v1.5 regression payload" :: B.ByteString
encrypted <- (P15.encrypt publicKey plaintext :: IO (Either RSA.Error B.ByteString))
ciphertext <-
case encrypted of
Left err ->
assertFailure ("RSA PKCS#1 v1.5 encryption failed: " ++ show err) >> pure mempty
Right ct -> pure ct
decrypted <- (P15.decryptSafer privateKey ciphertext :: IO (Either RSA.Error B.ByteString))
case decrypted of
Left RSA.MessageNotRecognized ->
assertFailure "parsed RSA private key decryption failed with MessageNotRecognized"
Left err ->
assertFailure ("parsed RSA private key decryption failed: " ++ show err)
Right got ->
assertEqual
"parsed RSA private key decrypts PKCS#1 v1.5 payload"
plaintext
got
testEncodeOpenPGPSessionMaterial :: Assertion
testEncodeOpenPGPSessionMaterial = do
let keyBytes = B.pack [1 .. 32]
expected =
B.singleton (fromFVal AES256) <> keyBytes <> encodeChecksum16 keyBytes
case encodeOpenPGPSessionMaterial AES256 (SessionKey keyBytes) of
Left err ->
assertFailure ("encodeOpenPGPSessionMaterial failed: " ++ show err)
Right encoded ->
assertEqual "OpenPGP session material encoding" expected encoded
mkPKESKSessionMaterialOrFail ::
SymmetricAlgorithm -> SessionKey -> IO PKESKSessionMaterial
mkPKESKSessionMaterialOrFail symalgo sessionKey =
case mkPKESKSessionMaterial symalgo sessionKey of
Left err ->
assertFailure ("mkPKESKSessionMaterial failed: " ++ show err) >>
fail "mkPKESKSessionMaterial failed"
Right material -> pure material
armorPayload :: Armor -> BL.ByteString
armorPayload (Armor _ _ bs) = BL.fromStrict (BLC8.toStrict bs)
armorPayload (ClearSigned _ _ inner) = armorPayload inner
selectRecipientKeyInfo :: Pkt -> [PKESKRecipientKey] -> Maybe PKESKRecipientKey
selectRecipientKeyInfo (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka _))) keyInfos =
listToMaybe
[ keyInfo
| keyInfo <- keyInfos
, supportsPKESKAlgorithm pka keyInfo
, matchesRecipientIdentifier rid keyInfo
]
selectRecipientKeyInfo _ keyInfos = listToMaybe keyInfos
supportsPKESKAlgorithm :: PubKeyAlgorithm -> PKESKRecipientKey -> Bool
supportsPKESKAlgorithm pka keyInfo =
case pkeskRecipientSKey keyInfo of
RSAPrivateKey {} -> pka == RSA
ECDHPrivateKey {} -> pka == ECDH || pka == X25519
X25519PrivateKey {} -> pka == X25519
X448PrivateKey {} -> pka == X448
_ -> False
matchesRecipientIdentifier :: BL.ByteString -> PKESKRecipientKey -> Bool
matchesRecipientIdentifier rid keyInfo =
case pkeskRecipientPKPayload keyInfo of
Nothing -> False
Just pkp ->
let fingerprintBytes = BL.toStrict (unFingerprint (fingerprint pkp))
identifier = BL.toStrict rid
in identifier == fingerprintBytes ||
identifier == B.cons 0x04 fingerprintBytes ||
identifier == B.cons 0x06 fingerprintBytes
buildECDHKDFParamForTest ::
SomePKPayload
-> PubKeyAlgorithm
-> ECCT.Curve
-> HashAlgorithm
-> SymmetricAlgorithm
-> B.ByteString
buildECDHKDFParamForTest recipientPKP pka curve kdfHA kdfSA =
B.singleton (fromIntegral (B.length curveOid)) <> curveOid <>
B.pack [fromFVal pka, 0x03, 0x01, fromFVal kdfHA, fromFVal kdfSA] <>
"Anonymous Sender " <>
BL.toStrict (unFingerprint (fingerprint recipientPKP))
where
curveOid = either (const B.empty) id (curveToCurveoidBS (curveFromCurve curve))
buildCurve25519LegacyKdfParamForTest ::
SomePKPayload
-> PubKeyAlgorithm
-> HashAlgorithm
-> SymmetricAlgorithm
-> B.ByteString
buildCurve25519LegacyKdfParamForTest recipientPKP pka kdfHA kdfSA =
B.singleton (fromIntegral (B.length curveOid)) <> curveOid <>
B.pack [fromFVal pka, 0x03, 0x01, fromFVal kdfHA, fromFVal kdfSA] <>
"Anonymous Sender " <>
BL.toStrict (unFingerprint (fingerprint recipientPKP))
where
curveOid = "\x2b\x06\x01\x04\x01\x97\x55\x01\x05\x01"
deriveECDHKekForTest ::
HashAlgorithm
-> SymmetricAlgorithm
-> B.ByteString
-> B.ByteString
-> B.ByteString
deriveECDHKekForTest kdfHA kdfSA sharedSecret kdfParam =
B.take (keyLengthForTest kdfSA) digest
where
digest =
case kdfHA of
SHA256 -> BA.convert (CH.hash (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam) :: CH.Digest CHA.SHA256)
SHA384 -> BA.convert (CH.hash (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam) :: CH.Digest CHA.SHA384)
SHA512 -> BA.convert (CH.hash (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam) :: CH.Digest CHA.SHA512)
_ -> BA.convert (CH.hash (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam) :: CH.Digest CHA.SHA256)
deriveX448KekForTest ::
B.ByteString
-> B.ByteString
-> B.ByteString
-> B.ByteString
deriveX448KekForTest ephemeralPublic recipientPublic sharedSecret =
let ikm = ephemeralPublic <> recipientPublic <> sharedSecret
prk = extract @CHA.SHA512 B.empty ikm
info = "OpenPGP X448" :: B.ByteString
in expand @CHA.SHA512 prk info 32
deriveX25519KekForTest ::
B.ByteString
-> B.ByteString
-> B.ByteString
-> B.ByteString
deriveX25519KekForTest ephemeralPublic recipientPublic sharedSecret =
let ikm = ephemeralPublic <> recipientPublic <> sharedSecret
prk = extract @CHA.SHA256 B.empty ikm
info = "OpenPGP X25519" :: B.ByteString
in expand @CHA.SHA256 prk info 16
keyLengthForTest :: SymmetricAlgorithm -> Int
keyLengthForTest AES128 = 16
keyLengthForTest AES192 = 24
keyLengthForTest AES256 = 32
keyLengthForTest _ = 16
aesKeyWrapRFC3394ForTest ::
SymmetricAlgorithm -> B.ByteString -> B.ByteString -> B.ByteString
aesKeyWrapRFC3394ForTest sa kek plain =
case sa of
AES128 -> wrapWithCipher (initCipher kek :: AES.AES128) plain
AES192 -> wrapWithCipher (initCipher kek :: AES.AES192) plain
AES256 -> wrapWithCipher (initCipher kek :: AES.AES256) plain
_ -> error "unsupported KEK algorithm in test"
where
initCipher keyBytes =
case CE.eitherCryptoError (CCT.cipherInit keyBytes) of
Left err -> error ("cipher init failed: " ++ show err)
Right c -> c
wrapWithCipher cipher plainBytes =
let rs = chunksOf8ForTest plainBytes
n = length rs
a0 = B.replicate 8 0xA6
(aFinal, rFinal) = foldl (\(a, r) j -> wrapRound cipher n j a r) (a0, rs) [0 .. 5]
in aFinal <> B.concat rFinal
wrapRound cipher n j a rs = foldl step (a, rs) [1 .. n]
where
step (aCurr, rCurr) i =
let b = CCT.ecbEncrypt cipher (aCurr <> (rCurr !! (i - 1)))
(aMsb, rLsb) = B.splitAt 8 b
t = fromIntegral (n * j + i) :: Word64
aNext = xorBSForTest aMsb (encodeWord64beForTest t)
in (aNext, replaceAtForTest (i - 1) rLsb rCurr)
encodeWord64beForTest :: Word64 -> B.ByteString
encodeWord64beForTest = BL.toStrict . runPut . putWord64be
chunksOf8ForTest :: B.ByteString -> [B.ByteString]
chunksOf8ForTest bs
| B.null bs = []
| otherwise =
let (h, t) = B.splitAt 8 bs
in h : chunksOf8ForTest t
replaceAtForTest :: Int -> a -> [a] -> [a]
replaceAtForTest idx x xs =
let (prefix, suffix) = splitAt idx xs
in case suffix of
[] -> xs
(_:rest) -> prefix <> (x : rest)
xorBSForTest :: B.ByteString -> B.ByteString -> B.ByteString
xorBSForTest a b = B.pack (B.zipWith xor a b)
encodeChecksum16 :: B.ByteString -> B.ByteString
encodeChecksum16 bs =
B.pack
[fromIntegral (s `div` 256), fromIntegral (s `mod` 256)]
where
s = B.foldl' (\acc octet -> (acc + fromIntegral octet) `mod` (65536 :: Int)) 0 bs
verifyMessageFromPackets :: PublicKeyring -> BL.ByteString -> [Either String Verification]
verifyMessageFromPackets keyring signedMessage =
map (either (Left . renderVerificationError) Right) $
verifyMessagePackets
defaultVerificationOptions
{ verificationPolicy = VerifyInformational
, verificationMode = VerificationStreaming
}
keyring
(concatMap (either (const []) id . decompressPkt) (parsePkts signedMessage))
verifyMessageFromPacketsBatch :: PublicKeyring -> BL.ByteString -> [Either String Verification]
verifyMessageFromPacketsBatch keyring signedMessage =
map (either (Left . renderVerificationError) Right) $
verifyMessagePackets
defaultVerificationOptions
{ verificationPolicy = VerifyInformational
, verificationMode = VerificationBatch
}
keyring
(concatMap (either (const []) id . decompressPkt) (parsePkts signedMessage))
verifyMessageFromBytestring :: PublicKeyring -> BL.ByteString -> [Either String Verification]
verifyMessageFromBytestring keyring signedMessage =
map (either (Left . renderVerificationError) Right) $
verifyMessage
defaultVerificationOptions
{ verificationPolicy = VerifyInformational
, verificationMode = VerificationStreaming
}
keyring
signedMessage
verifyMessageFromBytestringBatch :: PublicKeyring -> BL.ByteString -> [Either String Verification]
verifyMessageFromBytestringBatch keyring signedMessage =
map (either (Left . renderVerificationError) Right) $
verifyMessage
defaultVerificationOptions
{ verificationPolicy = VerifyInformational
, verificationMode = VerificationBatch
}
keyring
signedMessage
assertMessageVerification ::
(PublicKeyring -> BL.ByteString -> [Either String Verification])
-> FilePath
-> FilePath
-> [Fingerprint]
-> Assertion
assertMessageVerification verifier keyringFile messageFile issuers = do
kr <- loadKeyring keyringFile
signedMessage <- readFixtureLazy messageFile
let verification = verifier kr signedMessage
actual = map (fmap (fingerprint . _verificationSigner)) verification
assertEqual
(keyringFile ++ " for " ++ messageFile)
(map Right issuers)
actual
loadKeyring :: FilePath -> IO PublicKeyring
loadKeyring keyring =
DC.runConduitRes $
CB.sourceFile (fixturePath keyring) DC..| conduitGet get DC..| conduitToPublicViewTKs DC..|
sinkPublicKeyringMap
loadAndDecompressPkts :: FilePath -> IO [Pkt]
loadAndDecompressPkts = readFixtureDecompressedPackets
loadArmor :: FilePath -> IO [Armor]
loadArmor file = do
armored <- readFixtureLazy file
case AA.decodeLazy armored :: Either String [Armor] of
Left err ->
assertFailure ("Failed to decode armored fixture " ++ file ++ ": " ++ err) >> pure []
Right armors -> pure armors
encryptMessageDefault ::
SessionMaterialExposure
-> Passphrase
-> ClearPayload
-> Either
MessageError
(EncryptedPayload, Maybe RecoveredSessionMaterial)
encryptMessageDefault exposure passphrase payload =
encryptMessage
(RFC9580EncryptMessageOptions
{ rfc9580EncryptMessageExposure = exposure
, rfc9580EncryptMessageSymmetricAlgorithm = AES256
, rfc9580EncryptMessageS2K = Argon2 (Salt16 (B.pack [0x80 .. 0x8f])) 1 4 15
, rfc9580EncryptMessageIV = IV "0123456789ABCDEF"
})
passphrase
payload
mkTestKeyring :: [TKUnknown] -> PublicKeyring
mkTestKeyring tks =
let someTKs = [stk | Right stk <- map fromUnknownToTK tks]
in fst (partitionSomeTKs someTKs)
addTimestampSeconds :: ThirtyTwoBitTimeStamp -> Word32 -> ThirtyTwoBitTimeStamp
addTimestampSeconds (ThirtyTwoBitTimeStamp ts) seconds = ThirtyTwoBitTimeStamp (ts + seconds)
timestampToUTCTime :: ThirtyTwoBitTimeStamp -> UTCTime
timestampToUTCTime = posixSecondsToUTCTime . realToFrac . unThirtyTwoBitTimeStamp
signCertificationAt ::
SomePKPayload
-> RSA.PrivateKey
-> UserId
-> ThirtyTwoBitTimeStamp
-> [SigSubPacket]
-> IO SignaturePayload
signCertificationAt signer signingKey uid creationTime hashedExtras = do
(hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
case signCertificationWithRSA
GenericCert
signer
uid
(hashedExtras ++ hashed)
unhashed
signingKey of
Left err ->
assertFailure ("failed to sign certification: " ++ renderSignError err) >>
fail "expected certification signature"
Right sigPayload -> pure sigPayload
signCertificationRevocationAt ::
SomePKPayload
-> RSA.PrivateKey
-> UserId
-> ThirtyTwoBitTimeStamp
-> [SigSubPacket]
-> IO SignaturePayload
signCertificationRevocationAt signer signingKey uid creationTime hashedExtras = do
(hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
case signCertRevocationWithRSA
signer
uid
(hashedExtras ++ hashed)
unhashed
signingKey of
Left err ->
assertFailure ("failed to sign certification revocation: " ++ renderSignError err) >>
fail "expected certification revocation signature"
Right sigPayload -> pure sigPayload
messageIssuerSubpacketsAt ::
SomePKPayload
-> ThirtyTwoBitTimeStamp
-> IO ([SigSubPacket], [SigSubPacket])
messageIssuerSubpacketsAt signer creationTime =
case eightOctetKeyID signer of
Left err ->
assertFailure ("failed to derive issuer key id for timeline test: " ++ err) >>
fail "expected issuer key id"
Right issuerKeyId ->
pure
( [ SigSubPacket False (SigCreationTime creationTime)
, SigSubPacket False (IssuerFingerprint IssuerFingerprintV4 (fingerprint signer))
]
, [SigSubPacket False (Issuer issuerKeyId)]
)
loadUnencryptedRsaSigner :: IO (SomePKPayload, RSA.PrivateKey)
loadUnencryptedRsaSigner = do
secretPackets <-
DC.runConduitRes $
CB.sourceFile "tests/data/unencrypted.seckey" DC..| conduitGet get DC..| CL.consume
case secretPackets of
(SecretKeyPkt pkp ska:_) ->
case ska of
SUUnencrypted (RSAPrivateKey (RSA_PrivateKey privateKey)) _ ->
pure (pkp, privateKey)
_ ->
assertFailure "unencrypted.seckey did not contain an unencrypted RSA key" >>
fail "expected decrypted RSA private key"
_ ->
assertFailure "unencrypted.seckey did not begin with a secret key packet" >>
fail "expected secret key packet"
loadUnencryptedRsaSignerV6 :: IO (SomePKPayload, RSA.PrivateKey)
loadUnencryptedRsaSignerV6 = do
(signer, signingKey) <- loadUnencryptedRsaSigner
pure (setKeyVersion V6 signer, signingKey)
setKeyVersion :: KeyVersion -> SomePKPayload -> SomePKPayload
setKeyVersion keyVersion (PKPayload _ ts v3e pka pubkey) =
PKPayload keyVersion ts v3e pka pubkey
setKeyTimestamp :: ThirtyTwoBitTimeStamp -> SomePKPayload -> SomePKPayload
setKeyTimestamp ts (PKPayload keyVersion _ v3e pka pubkey) =
PKPayload keyVersion ts v3e pka pubkey
setPKAlgorithm :: PubKeyAlgorithm -> SomePKPayload -> SomePKPayload
setPKAlgorithm algorithm (PKPayload keyVersion ts v3e _ pubkey) =
PKPayload keyVersion ts v3e algorithm pubkey
expectV4PKPayload :: String -> SomePKPayload -> IO (VersionedPKPayload V4)
expectV4PKPayload label pk =
case asV4PKPayload pk of
Left err ->
assertFailure (label ++ " should have a v4 PKPayload: " ++ err) >>
fail "expected v4 PKPayload"
Right v4pk -> pure v4pk
expectV6PKPayload :: String -> SomePKPayload -> IO (VersionedPKPayload V6)
expectV6PKPayload label pk =
case asV6PKPayload pk of
Left err ->
assertFailure (label ++ " should have a v6 PKPayload: " ++ err) >>
fail "expected v6 PKPayload"
Right v6pk -> pure v6pk
loadDeterministicEd25519Signer :: IO (SomePKPayload, Ed25519.SecretKey)
loadDeterministicEd25519Signer = do
let seed = B.pack [1 .. 32]
secretKey <-
case CE.eitherCryptoError (Ed25519.secretKey seed) of
Left err ->
assertFailure ("failed to initialize deterministic Ed25519 secret key: " ++ show err) >>
fail "expected deterministic Ed25519 secret key"
Right sk -> pure sk
let publicKeyBytes = BA.convert (Ed25519.toPublic secretKey) :: B.ByteString
signer =
PKPayload
V4
0
0
EdDSA
(EdDSAPubKey Ed25519 (PrefixedNativeEPoint (EPoint (os2ip (B.cons 0x40 publicKeyBytes)))))
pure (signer, secretKey)
loadDeterministicEd25519SignerV6 :: IO (SomePKPayload, Ed25519.SecretKey)
loadDeterministicEd25519SignerV6 = do
let seed = B.pack [1 .. 32]
secretKey <-
case CE.eitherCryptoError (Ed25519.secretKey seed) of
Left err ->
assertFailure ("failed to initialize deterministic Ed25519 secret key: " ++ show err) >>
fail "expected deterministic Ed25519 secret key"
Right sk -> pure sk
let publicKeyBytes = BA.convert (Ed25519.toPublic secretKey) :: B.ByteString
signer =
PKPayload
V6
0
0
EdDSA
(EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip publicKeyBytes))))
pure (signer, secretKey)
loadDeterministicEd448Signer :: IO (SomePKPayload, Ed448.SecretKey)
loadDeterministicEd448Signer = do
let seed = B.pack [1 .. 57]
secretKey <-
case CE.eitherCryptoError (Ed448.secretKey seed) of
Left err ->
assertFailure ("failed to initialize deterministic Ed448 secret key: " ++ show err) >>
fail "expected deterministic Ed448 secret key"
Right sk -> pure sk
let publicKeyBytes = BA.convert (Ed448.toPublic secretKey) :: B.ByteString
signer =
PKPayload
V4
0
0
EdDSA
(EdDSAPubKey Ed448 (PrefixedNativeEPoint (EPoint (os2ip (B.cons 0x40 publicKeyBytes)))))
pure (signer, secretKey)
loadDeterministicEd448SignerV6 :: IO (SomePKPayload, Ed448.SecretKey)
loadDeterministicEd448SignerV6 = do
let seed = B.pack [1 .. 57]
secretKey <-
case CE.eitherCryptoError (Ed448.secretKey seed) of
Left err ->
assertFailure ("failed to initialize deterministic Ed448 secret key: " ++ show err) >>
fail "expected deterministic Ed448 secret key"
Right sk -> pure sk
let publicKeyBytes = BA.convert (Ed448.toPublic secretKey) :: B.ByteString
signer =
PKPayload
V6
0
0
EdDSA
(EdDSAPubKey Ed448 (NativeEPoint (EPoint (os2ip publicKeyBytes))))
pure (signer, secretKey)
testSEIPDv2ForV4KeyArmor :: Assertion
testSEIPDv2ForV4KeyArmor = do
armors <- loadArmor "seipdv2-for-v4-key.pgp.aa"
armor <-
case armors of
[a] -> pure a
_ ->
assertFailure "seipdv2-for-v4-key fixture should contain one armored payload" >>
fail "expected one armored payload"
payload <-
case armor of
Armor ArmorMessage _ p -> pure p
Armor atype _ _ ->
assertFailure
("seipdv2-for-v4-key fixture should decode as a message block, got " ++
show atype) >>
fail "expected message block"
_ ->
assertFailure "seipdv2-for-v4-key fixture should decode as an armored payload" >>
fail "expected armored payload"
let packets = parsePkts (BL.fromStrict (BLC8.toStrict payload))
case packets of
[PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka _)), SymEncIntegrityProtectedDataPkt (SEIPD2 sa aa chunkSize _ _)] -> do
let ridHex = map toUpper (BLC8.unpack (B16L.encode rid))
if ridHex `elem` ["C8263FC6D676044B6E973959C2F2C2CAE30DE908", "04C8263FC6D676044B6E973959C2F2C2CAE30DE908"]
then pure ()
else
assertFailure
("seipdv2-for-v4-key fixture should target the expected recipient fingerprint, got " ++
ridHex)
assertEqual
"seipdv2-for-v4-key fixture should use ECDH PKESKv6"
ECDH
pka
assertEqual
"seipdv2-for-v4-key fixture should use AES-256"
AES256
sa
assertEqual
"seipdv2-for-v4-key fixture should use OCB"
OCB
aa
assertEqual
"seipdv2-for-v4-key fixture should use 4KiB chunks"
6
chunkSize
_ ->
assertFailure
("seipdv2-for-v4-key fixture should contain [PKESKPkt (PKESK6 ...), SymEncIntegrityProtectedDataPkt (SEIPD2 ...)], got: " ++
show packets)
loadSEIPDv2FixtureWithV4Secret :: FilePath -> IO ([Pkt], [Pkt], BL.ByteString)
loadSEIPDv2FixtureWithV4Secret fixture = do
messageArmor <- loadFirstArmor fixture
encryptedSecretArmor <- loadFirstArmor "v4-encrypted-secret.pgp.aa"
passphrase <- readPKIPassphrase
pure
( parsePkts (armorPayload messageArmor)
, parsePkts (armorPayload encryptedSecretArmor)
, passphrase
)
forceVersionedRecipientIdentifier :: Pkt -> Pkt
forceVersionedRecipientIdentifier pkt =
case pkt of
PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka esk))
| BL.length rid == 20 ->
PKESKPkt
(PKESKPayloadV6Packet (PKESKPayloadV6 (BL.cons 0x04 rid) pka esk))
| BL.length rid == 32 ->
PKESKPkt
(PKESKPayloadV6Packet (PKESKPayloadV6 (BL.cons 0x06 rid) pka esk))
| otherwise -> pkt
_ -> pkt
selectRecipientKeyInfoByRawRecipientId :: Pkt -> [PKESKRecipientKey] -> Maybe PKESKRecipientKey
selectRecipientKeyInfoByRawRecipientId (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka _))) keyInfos =
listToMaybe
[ keyInfo
| keyInfo <- keyInfos
, supportsPKESKAlgorithm pka keyInfo
, matchesRawRecipientFingerprint rid keyInfo
]
selectRecipientKeyInfoByRawRecipientId _ keyInfos = listToMaybe keyInfos
matchesRawRecipientFingerprint :: BL.ByteString -> PKESKRecipientKey -> Bool
matchesRawRecipientFingerprint rid keyInfo =
case pkeskRecipientPKPayload keyInfo of
Nothing -> False
Just pkp ->
BL.toStrict rid == BL.toStrict (unFingerprint (fingerprint pkp))
testSEIPDv2TwoRecipientsArmor :: Assertion
testSEIPDv2TwoRecipientsArmor =
testSEIPDv2RecipientFixtureArmor "seipdv2-two-recipients.pgp.aa" 2
testSEIPDv2ThreeRecipientsArmor :: Assertion
testSEIPDv2ThreeRecipientsArmor =
testSEIPDv2RecipientFixtureArmor "seipdv2-three-recipients.pgp.aa" 3
testSEIPDv2RecipientFixtureArmor :: FilePath -> Int -> Assertion
testSEIPDv2RecipientFixtureArmor file expectedRecipients = do
armors <- loadArmor file
armor <-
case armors of
[a] -> pure a
_ ->
assertFailure (file ++ " fixture should contain one armored payload") >>
fail "expected one armored payload"
payload <-
case armor of
Armor ArmorMessage _ p -> pure p
Armor atype _ _ ->
assertFailure
(file ++ " fixture should decode as a message block, got " ++ show atype) >>
fail "expected message block"
_ ->
assertFailure (file ++ " fixture should decode as an armored payload") >>
fail "expected armored payload"
let packets = parsePkts (BL.fromStrict (BLC8.toStrict payload))
pkesks = [(rid, pka) | PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka _)) <- packets]
x25519Esks = [BL.toStrict esk | PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 _ X25519 esk)) <- packets]
x448Esks = [BL.toStrict esk | PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 _ X448 esk)) <- packets]
seipdv2Packets =
[(sa, aa, chunkSize) | SymEncIntegrityProtectedDataPkt (SEIPD2 sa aa chunkSize _ _) <- packets]
assertEqual
(file ++ " fixture should contain expected number of PKESKv6 packets")
expectedRecipients
(length pkesks)
if all (\(_, pka) -> pka == ECDH || pka == X25519) pkesks
then pure ()
else
assertFailure
(file ++ " fixture should use only ECDH/X25519 PKESKv6 packets, got " ++
show (map snd pkesks))
if any ((== ECDH) . snd) pkesks
then pure ()
else
assertFailure (file ++ " fixture should include an ECDH recipient for the v4 key")
mapM_ (assertX25519EskShape file) x25519Esks
mapM_ (assertX448EskShape file) x448Esks
if all
(\(rid, _) ->
let l = BL.length rid
in l == 20 || l == 21 || l == 32 || l == 33)
pkesks
then pure ()
else
assertFailure
(file ++ " fixture should use 20/21-byte or 32/33-byte recipient identifiers")
case seipdv2Packets of
[(sa, aa, chunkSize)] -> do
assertEqual (file ++ " fixture should use AES-256") AES256 sa
assertEqual (file ++ " fixture should use OCB") OCB aa
assertEqual (file ++ " fixture should use 4KiB chunks") 6 chunkSize
_ ->
assertFailure
(file ++ " fixture should contain one SymEncIntegrityProtectedDataV2 packet")
assertX25519EskShape :: FilePath -> B.ByteString -> Assertion
assertX25519EskShape file x25519Esk
| B.length x25519Esk < 33 =
assertFailure
(file ++ " X25519 PKESKv6 ESK should contain 32-octet ephemeral and wrapped-len")
| otherwise = do
let wrappedLen = fromIntegral (B.index x25519Esk 32) :: Int
wrapped = B.drop 33 x25519Esk
assertEqual
(file ++ " X25519 PKESKv6 wrapped length octet")
wrappedLen
(B.length wrapped)
assertX448EskShape :: FilePath -> B.ByteString -> Assertion
assertX448EskShape file x448Esk
| B.length x448Esk < 57 =
assertFailure
(file ++ " X448 PKESKv6 ESK should contain 56-octet ephemeral and wrapped-len")
| otherwise = do
let wrappedLen = fromIntegral (B.index x448Esk 56) :: Int
wrapped = B.drop 57 x448Esk
assertEqual
(file ++ " X448 PKESKv6 wrapped length octet")
wrappedLen
(B.length wrapped)
prependUnusableLatestPKESK :: [Pkt] -> [Pkt]
prependUnusableLatestPKESK packets =
let bogusRid = BL.pack (0x06 : replicate 32 0x99)
bogusPKESK = PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 bogusRid RSA "bogus-esk"))
(eskPrefix, encryptedSuffix) = span isPrecedingESK packets
in eskPrefix ++ [bogusPKESK] ++ encryptedSuffix
reorderPrecedingPKESKs :: [Pkt] -> [Pkt]
reorderPrecedingPKESKs packets =
let (eskPrefix, encryptedSuffix) = span isPrecedingESK packets
reorderedPKESKs = reverse [pkt | pkt@(PKESKPkt _) <- eskPrefix]
in refillPKESKSlots eskPrefix reorderedPKESKs ++ encryptedSuffix
where
refillPKESKSlots [] _ = []
refillPKESKSlots (pkt:rest) pkesks =
case pkt of
PKESKPkt {} ->
case pkesks of
[] -> pkt : refillPKESKSlots rest []
(replacement:remaining) ->
replacement : refillPKESKSlots rest remaining
_ -> pkt : refillPKESKSlots rest pkesks
isPrecedingESK :: Pkt -> Bool
isPrecedingESK (PKESKPkt _) = True
isPrecedingESK (SKESKPkt _) = True
isPrecedingESK _ = False
messageVerificationFixtures :: [(String, FilePath, FilePath, [Fingerprint])]
messageVerificationFixtures =
[ ( "uncompressed-ops-dsa"
, "pubring.gpg"
, "uncompressed-ops-dsa.gpg"
, [fp "1EB2 0B2F 5A5C C3BE AFD6 E5CB 7732 CF98 8A63 EA86"])
, ( "uncompressed-ops-dsa-sha384"
, "pubring.gpg"
, "uncompressed-ops-dsa-sha384.txt.gpg"
, [fp "1EB2 0B2F 5A5C C3BE AFD6 E5CB 7732 CF98 8A63 EA86"])
, ( "uncompressed-ops-rsa"
, "pubring.gpg"
, "uncompressed-ops-rsa.gpg"
, [fp "CB79 3345 9F59 C70D F1C3 FBEE DEDC 3ECF 689A F56D"])
, ( "compressedsig"
, "pubring.gpg"
, "compressedsig.gpg"
, [fp "421F 28FE AAD2 22F8 56C8 FFD5 D4D5 4EA1 6F87 040E"])
, ( "compressedsig-zlib"
, "pubring.gpg"
, "compressedsig-zlib.gpg"
, [fp "421F 28FE AAD2 22F8 56C8 FFD5 D4D5 4EA1 6F87 040E"])
, ( "compressedsig-bzip2"
, "pubring.gpg"
, "compressedsig-bzip2.gpg"
, [fp "421F 28FE AAD2 22F8 56C8 FFD5 D4D5 4EA1 6F87 040E"])
]
certificateVerificationFixtures :: [(String, FilePath, FilePath, [Fingerprint])]
certificateVerificationFixtures =
[ ( "userid"
, "pubring.gpg"
, "minimized.gpg"
, [fp "421F 28FE AAD2 22F8 56C8 FFD5 D4D5 4EA1 6F87 040E"])
, ( "subkey"
, "pubring.gpg"
, "subkey.gpg"
, [fp "421F 28FE AAD2 22F8 56C8 FFD5 D4D5 4EA1 6F87 040E"])
, ( "primary key binding"
, "signing-subkey.gpg"
, "primary-binding.gpg"
, [fp "ED1B D216 F70E 5D5F 4444 48F9 B830 F2C4 83A9 9AE5"])
, ( "attribute"
, "pubring.gpg"
, "uat.gpg"
, [fp "421F 28FE AAD2 22F8 56C8 FFD5 D4D5 4EA1 6F87 040E"])
, ( "primary key revocation"
, "pubring.gpg"
, "prikey-rev.gpg"
, [fp "421F 28FE AAD2 22F8 56C8 FFD5 D4D5 4EA1 6F87 040E"])
, ( "subkey revocation"
, "pubring.gpg"
, "subkey-rev.gpg"
, [fp "421F 28FE AAD2 22F8 56C8 FFD5 D4D5 4EA1 6F87 040E"])
, ( "6F87040E"
, "pubring.gpg"
, "6F87040E.pubkey"
, [ fp "421F 28FE AAD2 22F8 56C8 FFD5 D4D5 4EA1 6F87 040E"
, fp "CB79 3345 9F59 C70D F1C3 FBEE DEDC 3ECF 689A F56D"
, fp "AF95 E4D7 BAC5 21EE 9740 BED7 5E9F 1523 4132 62DC"
])
, ( "6F87040E-cr"
, "pubring.gpg"
, "6F87040E-cr.pubkey"
, [ fp "AF95 E4D7 BAC5 21EE 9740 BED7 5E9F 1523 4132 62DC"
, fp "AF95 E4D7 BAC5 21EE 9740 BED7 5E9F 1523 4132 62DC"
, fp "421F 28FE AAD2 22F8 56C8 FFD5 D4D5 4EA1 6F87 040E"
, fp "CB79 3345 9F59 C70D F1C3 FBEE DEDC 3ECF 689A F56D"
, fp "AF95 E4D7 BAC5 21EE 9740 BED7 5E9F 1523 4132 62DC"
])
, ( "simple RSA secret key"
, "pubring.gpg"
, "simple.seckey"
, [fp "421F 28FE AAD2 22F8 56C8 FFD5 D4D5 4EA1 6F87 040E"])
, ( "simple ECDSA public key"
, "ecdsa-key-without-ecdh.pubkey"
, "ecdsa-key-without-ecdh.pubkey"
, [fp "174C CF12 C571 6D0E 527F B50E F770 8BAD D606 3224"])
]
verificationFixtureGroup ::
String
-> (PublicKeyring -> BL.ByteString -> [Either String Verification])
-> [(String, FilePath, FilePath, [Fingerprint])]
-> TestTree
verificationFixtureGroup groupName verifier fixtures =
testGroup
groupName
[ testCase name (assertMessageVerification verifier keyringFile messageFile issuers)
| (name, keyringFile, messageFile, issuers) <- fixtures
]
loadV6UnencryptedSecretKeyFixtureForProperty :: IO (Either String (SomePKPayload, SKAddendum, SKey))
loadV6UnencryptedSecretKeyFixtureForProperty = do
armored <- readFixtureLazy "v6-secret.pgp.aa"
pure $ do
armors <- first ("failed to decode v6 secret fixture: " ++) (AA.decodeLazy armored)
armor <-
case armors of
(a:_) -> Right a
[] -> Left "v6-secret.pgp.aa should contain one armored payload"
let packets = parsePkts (armorPayload armor)
(pkp, ska) <-
case packets of
(SecretKeyPkt pkpayload skaddendum:_) -> Right (pkpayload, skaddendum)
_ -> Left "v6-secret.pgp.aa should begin with a secret key packet"
skey <-
case ska of
SUUnencrypted x _ -> Right x
_ -> Left "v6-secret.pgp.aa should contain unencrypted secret key material"
Right (pkp, ska, skey)
loadV4EncryptedSecretKeyFixtureForProperty ::
IO (Either String (SomePKPayload, SKAddendum, SKey, BL.ByteString))
loadV4EncryptedSecretKeyFixtureForProperty = do
passphrase <- readPKIPassphrase
packets <-
DC.runConduitRes $
CB.sourceFile "tests/data/aes256-sha512.seckey" DC..| conduitGet get DC..|
CL.consume
pure $ do
(pkp, ska) <-
case packets of
(SecretKeyPkt pkpayload skaddendum:_) -> Right (pkpayload, skaddendum)
_ -> Left "aes256-sha512.seckey should begin with a secret key packet"
skey <-
case decryptPrivateKey (pkp, ska) passphrase of
Right (SUUnencrypted x _) -> Right x
Right other ->
Left ("unexpected decrypted key shape for v4 fixture: " ++ show other)
Left err ->
Left ("failed to decrypt v4 fixture secret key: " ++ err)
Right (pkp, ska, skey, passphrase)
reverseIf :: Bool -> [a] -> [a]
reverseIf shouldReverse xs
| shouldReverse = reverse xs
| otherwise = xs
cgp :: DC.ConduitT B.ByteString Pkt (ResourceT IO) ()
cgp = conduitGet (get :: Get Pkt)
fp :: Text -> Fingerprint
fp = either error id . parseFingerprint
doPkeyAndSkeyMatch :: PKey -> SKey -> Assertion
doPkeyAndSkeyMatch (RSAPubKey (RSA_PublicKey rpub)) (RSAPrivateKey (RSA_PrivateKey rpriv)) =
assertEqual
"RSA private key matches RSA public key"
rpub
(RSA.private_pub rpriv)
doPkeyAndSkeyMatch (ECDSAPubKey (ECDSA_PublicKey ecpub)) (ECDSAPrivateKey (ECDSA_PrivateKey ecpriv)) =
assertEqual
"ECDSA private key curve matches ECDSA public key curve"
(ECDSA.public_curve ecpub)
(ECDSA.private_curve ecpriv)
doPkeyAndSkeyMatch _ _ = assertFailure "matching unimplemented"
collectSecretKeyInfos :: [Pkt] -> BL.ByteString -> IO [PKESKRecipientKey]
collectSecretKeyInfos pkts passphrase = do
let keyInfoResults = map toKeyInfo pkts
keyInfos = catMaybes [mKeyInfo | Right mKeyInfo <- keyInfoResults]
unlockErrors = [err | Left err <- keyInfoResults]
unless (null unlockErrors) $
assertFailure
("one or more secret key packets failed to unlock (partial failures are surfaced to\
\ prevent silent key-context gaps):\n" ++
unlines unlockErrors)
pure keyInfos
where
toKeyInfo (SecretKeyPkt pkp ska) = decryptToRecipientKey "SecretKeyPkt" pkp ska
toKeyInfo (SecretSubkeyPkt pkp ska) = decryptToRecipientKey "SecretSubkeyPkt" pkp ska
toKeyInfo _ = Right Nothing
decryptToRecipientKey contextLabel pkp ska =
case ska of
SUUnencrypted skey _ ->
Right
(Just
(PKESKRecipientKey
{ pkeskRecipientPKPayload = Just pkp
, pkeskRecipientSKey = skey
}))
_ ->
case decryptPrivateKey (pkp, ska) passphrase of
Right (SUUnencrypted skey _) ->
Right
(Just
(PKESKRecipientKey
{ pkeskRecipientPKPayload = Just pkp
, pkeskRecipientSKey = skey
}))
Right decryptedSKA ->
Left
(contextLabel ++
" " ++
show (fingerprint pkp) ++
": decryptPrivateKey returned unexpected protection: " ++
show decryptedSKA)
Left err ->
Left
(contextLabel ++
" " ++
show (fingerprint pkp) ++
": decryptPrivateKey failed: " ++ err)
signSubkeyRevocationWithRSAAt ::
SomePKPayload
-> SomePKPayload
-> RSA.PrivateKey
-> ThirtyTwoBitTimeStamp
-> IO SignaturePayload
signSubkeyRevocationWithRSAAt signer subkey signingKey creationTime = do
(hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
case signSubkeyRevocationWithRSA signer subkey hashed unhashed signingKey of
Left err ->
assertFailure ("failed to sign subkey revocation: " ++ renderSignError err) >>
fail "expected subkey revocation signature"
Right sigPayload -> pure sigPayload
signSubkeyBindingWithRSAAt ::
SomePKPayload
-> SomePKPayload
-> RSA.PrivateKey
-> ThirtyTwoBitTimeStamp
-> IO SignaturePayload
signSubkeyBindingWithRSAAt signer subkey signingKey creationTime = do
signSubkeyBindingWithRSAExtrasAt signer subkey signingKey creationTime []
signDirectKeyWithRSAExtrasAt ::
SomePKPayload
-> RSA.PrivateKey
-> ThirtyTwoBitTimeStamp
-> [SigSubPacket]
-> IO SignaturePayload
signDirectKeyWithRSAExtrasAt signer signingKey creationTime hashedExtras = do
(hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
case signDirectKeyWithRSA SignatureDirectlyOnAKey signer (hashedExtras ++ hashed) unhashed signingKey of
Left err ->
assertFailure ("failed to sign direct key self-signature: " ++ renderSignError err) >>
fail "expected direct key self-signature"
Right sigPayload -> pure sigPayload
signSubkeyBindingWithRSAExtrasAt ::
SomePKPayload
-> SomePKPayload
-> RSA.PrivateKey
-> ThirtyTwoBitTimeStamp
-> [SigSubPacket]
-> IO SignaturePayload
signSubkeyBindingWithRSAExtrasAt signer subkey signingKey creationTime hashedExtras = do
(hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
let bindingPayload =
payloadForSig
SubkeyBindingSig
emptyPSC
{ lastPrimaryKey = PublicKeyPkt signer
, lastSubkey = PublicSubkeyPkt subkey
}
case signDataWithRSA SubkeyBindingSig signingKey (hashedExtras ++ hashed) unhashed bindingPayload of
Left err ->
assertFailure ("failed to sign subkey binding: " ++ renderSignError err) >>
fail "expected subkey binding signature"
Right sigPayload -> pure sigPayload
verifyTimelinePackets :: PublicKeyring -> BL.ByteString -> SignaturePayload -> [Either VerificationError Verification]
verifyTimelinePackets keyring payload sigPayload =
verifyMessagePackets
defaultVerificationOptions
{ verificationPolicy = VerifyStrict
, verificationMode = VerificationBatch
}
keyring
[LiteralDataPkt BinaryData BL.empty 0 payload, SignaturePkt sigPayload]
assertSingleSignerFingerprint ::
String
-> Fingerprint
-> [Either VerificationError Verification]
-> Assertion
assertSingleSignerFingerprint label expected results =
case results of
[Right verification] ->
assertEqual label expected (fingerprint (_verificationSigner verification))
other ->
assertFailure
(label ++
", expected one successful verification result, got " ++
show (length other) ++
" result(s)")
assertSingleFailureContainsTimeline ::
String
-> String
-> [Either VerificationError Verification]
-> Assertion
assertSingleFailureContainsTimeline label needle results =
case results of
[Left err] ->
assertBool
label
(needle `isInfixOf` renderVerificationError err)
other ->
assertFailure
(label ++
", expected one verification failure result, got " ++
show (length other) ++
" result(s)")
signCertificationWithEd25519At ::
SomePKPayload
-> SomePKPayload
-> Ed25519.SecretKey
-> UserId
-> ThirtyTwoBitTimeStamp
-> [SigSubPacket]
-> IO SignaturePayload
signCertificationWithEd25519At certifiedSigner certifierSigner signingKey uid creationTime hashedExtras = do
(hashed, unhashed) <- messageIssuerSubpacketsAt certifierSigner creationTime
let state =
emptyPSC
{ lastPrimaryKey = PublicKeyPkt certifiedSigner
, lastUIDorUAt = UserIdPkt (let UserId uidText = uid in uidText)
}
case signDataWithEd25519
GenericCert
signingKey
(hashedExtras ++ hashed)
unhashed
(payloadForSig GenericCert state) of
Left err ->
assertFailure ("failed to sign Ed25519 certification: " ++ renderSignError err) >>
fail "expected Ed25519 certification signature"
Right sigPayload -> pure sigPayload
signCertificationRevocationWithEd25519At ::
SomePKPayload
-> SomePKPayload
-> Ed25519.SecretKey
-> UserId
-> ThirtyTwoBitTimeStamp
-> [SigSubPacket]
-> IO SignaturePayload
signCertificationRevocationWithEd25519At certifiedSigner certifierSigner signingKey uid creationTime hashedExtras = do
(hashed, unhashed) <- messageIssuerSubpacketsAt certifierSigner creationTime
let state =
emptyPSC
{ lastPrimaryKey = PublicKeyPkt certifiedSigner
, lastUIDorUAt = UserIdPkt (let UserId uidText = uid in uidText)
}
case signDataWithEd25519
CertRevocationSig
signingKey
(hashedExtras ++ hashed)
unhashed
(payloadForSig CertRevocationSig state) of
Left err ->
assertFailure ("failed to sign Ed25519 certification revocation: " ++ renderSignError err) >>
fail "expected Ed25519 certification revocation signature"
Right sigPayload -> pure sigPayload
signKeyRevocationWithReasonAt ::
SomePKPayload
-> RSA.PrivateKey
-> ThirtyTwoBitTimeStamp
-> RevocationCode
-> IO SignaturePayload
signKeyRevocationWithReasonAt signer signingKey creationTime reasonCode =
signKeyRevocationWithReasonAndExtrasAt
signer
signingKey
creationTime
reasonCode
[]
signKeyRevocationWithReasonAndExtrasAt ::
SomePKPayload
-> RSA.PrivateKey
-> ThirtyTwoBitTimeStamp
-> RevocationCode
-> [SigSubPacket]
-> IO SignaturePayload
signKeyRevocationWithReasonAndExtrasAt signer signingKey creationTime reasonCode hashedExtras = do
(hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
case signKeyRevocationWithRSA
signer
(SigSubPacket False (ReasonForRevocation reasonCode "") : hashedExtras ++ hashed)
unhashed
signingKey of
Left err ->
assertFailure ("failed to sign key revocation: " ++ renderSignError err) >>
fail "expected key revocation signature"
Right sigPayload -> pure sigPayload
signBinaryMessageWithRSAAt ::
SomePKPayload
-> RSA.PrivateKey
-> ThirtyTwoBitTimeStamp
-> BL.ByteString
-> IO SignaturePayload
signBinaryMessageWithRSAAt signer signingKey creationTime payload = do
(hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
case signDataWithRSA BinarySig signingKey hashed unhashed payload of
Left err ->
assertFailure ("failed to sign RSA message payload: " ++ renderSignError err) >>
fail "expected RSA message signature"
Right sigPayload -> pure sigPayload
signBinaryMessageWithEd25519At ::
SomePKPayload
-> Ed25519.SecretKey
-> ThirtyTwoBitTimeStamp
-> BL.ByteString
-> IO SignaturePayload
signBinaryMessageWithEd25519At signer signingKey creationTime payload = do
(hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
case signDataWithEd25519 BinarySig signingKey hashed unhashed payload of
Left err ->
assertFailure ("failed to sign Ed25519 message payload: " ++ renderSignError err) >>
fail "expected Ed25519 message signature"
Right sigPayload -> pure sigPayload