hOpenPGP-3.0.0: tests/Tests/MessageAndArmor.hs
-- MessageAndArmor.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 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 (parsePkts, parsePktsEither)
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 (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
"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 ""))))
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 ()