hOpenPGP-3.0.0: Codec/Encryption/OpenPGP/Expirations.hs
-- Expirations.hs: OpenPGP (RFC9580) expiration checking
-- Copyright © 2014-2026 Clint Adams
-- This software is released under the terms of the Expat license.
-- (See the LICENSE file).
{-# LANGUAGE GADTs #-}
module Codec.Encryption.OpenPGP.Expirations
( KeyState(..)
, keyStateAt
, effectiveKeyPreferencesAt
, effectiveUIDPreferencesAt
, effectiveKeyPreferencesAtTimestamp
, effectiveUIDPreferencesAtTimestamp
, isTKTimeValid
, isPKTimeValidWithSelfSignatures
, getKeyExpirationTimesFromSignature
, isCertificationSig
, signatureCreationTime
, signatureExpirationTime
, signatureExpirationDuration
, firstSignatureExpirationDuration
, signatureEffectiveAt
, addDurationToTime
, newestByCreationTime
) where
import Control.Error.Util (hush)
import Control.Lens ((&), (^.), _1)
import Data.List (maximumBy)
import Data.Maybe (listToMaybe, mapMaybe)
import Data.Ord (comparing)
import Data.Text (Text)
import Data.Time.Clock (UTCTime, addUTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)
import Codec.Encryption.OpenPGP.Internal (issuer, issuerFP)
import Codec.Encryption.OpenPGP.Ontology (isKET)
import Codec.Encryption.OpenPGP.SignatureQualities
( sigCT
, sigType
, signatureHashedSubpacketsKnown
)
import Codec.Encryption.OpenPGP.Types
data KeyState =
KeyState
{ keyStateValid :: Bool
, keyStateSelfSignaturesKnown :: Bool
, keyStateHasEffectiveSelfSignature :: Bool
, keyStateExpirationTime :: Maybe UTCTime
}
deriving (Eq, Show)
-- this assumes that all key expiration time subpackets are valid
isTKTimeValid :: UTCTime -> TKUnknown -> Bool
isTKTimeValid ct = keyStateValid . keyStateAt ct
keyStateAt :: UTCTime -> TKUnknown -> KeyState
keyStateAt ct tk =
baseState {keyStateValid = keyStateValid baseState && bindingStateAllowsValidation}
where
baseState =
keyStateFromSelfSignaturesAt ct (tk ^. tkuKey . _1) relevantSelfSignatures
relevantSelfSignatures =
filter (isDirectKeySelfSigFor primaryKey) (tk ^. tkuRevs) ++
filter (isSelfCertificationFor primaryKey) (concatMap snd (tk ^. tkuUIDs)) ++
filter (isSelfCertificationFor primaryKey) (concatMap snd (tk ^. tkuUAts))
selfCertificationGroups =
map (filter (isSelfSignatureFor primaryKey) . snd) (tk ^. tkuUIDs) ++
map (filter (isSelfSignatureFor primaryKey) . snd) (tk ^. tkuUAts)
hasAnySelfCertification = any (any isCertificationSig) selfCertificationGroups
hasAnyActiveSelfCertification =
any (selfCertificationGroupActiveAt ct) selfCertificationGroups
bindingStateAllowsValidation =
not hasAnySelfCertification || hasAnyActiveSelfCertification
primaryKey = tk ^. tkuKey . _1
effectiveKeyPreferencesAt :: UTCTime -> TKUnknown -> Maybe [SigSubPacketPayload]
effectiveKeyPreferencesAt ct tk
| not (keyStateValid (keyStateAt ct tk)) = Nothing
| otherwise = do
sig <- latestEffectivePreferenceCarrierAt ct tk
let prefs = preferencePayloadsFromSignature sig
if null prefs
then Nothing
else Just prefs
effectiveUIDPreferencesAt :: UTCTime -> Text -> TKUnknown -> Maybe [SigSubPacketPayload]
effectiveUIDPreferencesAt ct uid tk
| not (keyStateValid (keyStateAt ct tk)) = Nothing
| otherwise = do
sigs <- lookup uid (tk ^. tkuUIDs)
cert <- latestActiveSelfCertificationAt ct (filter (isSelfSignatureFor primaryKey) sigs)
let prefs = preferencePayloadsFromSignature cert
if null prefs
then Nothing
else Just prefs
where
primaryKey = tk ^. tkuKey . _1
effectiveKeyPreferencesAtTimestamp ::
ThirtyTwoBitTimeStamp -> TKUnknown -> Maybe [SigSubPacketPayload]
effectiveKeyPreferencesAtTimestamp ts =
effectiveKeyPreferencesAt (posixSecondsToUTCTime (realToFrac (unThirtyTwoBitTimeStamp ts)))
effectiveUIDPreferencesAtTimestamp ::
ThirtyTwoBitTimeStamp -> Text -> TKUnknown -> Maybe [SigSubPacketPayload]
effectiveUIDPreferencesAtTimestamp ts uid =
effectiveUIDPreferencesAt
(posixSecondsToUTCTime (realToFrac (unThirtyTwoBitTimeStamp ts)))
uid
isPKTimeValidWithSelfSignatures ::
UTCTime -> SomePKPayload -> [SignaturePayload] -> Bool
isPKTimeValidWithSelfSignatures ct pkp sigs =
keyStateValid (keyStateFromSelfSignaturesAt ct pkp sigs)
keyStateFromSelfSignaturesAt ::
UTCTime -> SomePKPayload -> [SignaturePayload] -> KeyState
keyStateFromSelfSignaturesAt ct pkp sigs =
KeyState
{ keyStateValid =
ct >= keyCreationTime &&
selfSignatureStateAllowsValidation &&
maybe True (ct <) keyExpirationTime
, keyStateSelfSignaturesKnown = maybe False (const True) latestKnownSelfSignature
, keyStateHasEffectiveSelfSignature =
maybe False (signatureEffectiveAt ct) latestKnownSelfSignature
, keyStateExpirationTime = keyExpirationTime
}
where
keyCreationTime = _timestamp pkp & posixSecondsToUTCTime . realToFrac
knownSelfSignatures = filter (signatureCreatedAtOrBefore ct) sigs
selfSignatureStateAllowsValidation =
maybe (null sigs) (signatureEffectiveAt ct) latestKnownSelfSignature
latestKnownSelfSignature = snd <$> newestByCreationTime (mapMaybeSignatureCreationTime knownSelfSignatures)
keyExpirationTime = effectiveKeyExpirationTime ct pkp sigs
effectiveKeyExpirationTime ::
UTCTime -> SomePKPayload -> [SignaturePayload] -> Maybe UTCTime
effectiveKeyExpirationTime ct pkp sigs =
signatureExpirationDurationToUTCTime pkp =<< latestKnownExpirationDuration ct sigs
latestKnownExpirationDuration ::
UTCTime -> [SignaturePayload] -> Maybe ThirtyTwoBitDuration
latestKnownExpirationDuration ct sigs =
latestKnownSelfSignatureExpirationDuration ct =<<
(snd <$> newestByCreationTime (mapMaybeSignatureCreationTime (filter (signatureCreatedAtOrBefore ct) sigs)))
latestKnownSelfSignatureExpirationDuration ::
UTCTime -> SignaturePayload -> Maybe ThirtyTwoBitDuration
latestKnownSelfSignatureExpirationDuration ct sig
| signatureEffectiveAt ct sig = listToMaybe (getKeyExpirationTimesFromSignature sig)
| otherwise = Nothing
signatureEffectiveAt :: UTCTime -> SignaturePayload -> Bool
signatureEffectiveAt ct sig =
maybe False (<= ct) (signatureCreationTime sig) &&
maybe True (ct <) (signatureExpirationTime sig)
signatureCreatedAtOrBefore :: UTCTime -> SignaturePayload -> Bool
signatureCreatedAtOrBefore ct sig =
maybe False (<= ct) (signatureCreationTime sig)
signatureCreationTime :: SignaturePayload -> Maybe UTCTime
signatureCreationTime =
fmap (posixSecondsToUTCTime . realToFrac . unThirtyTwoBitTimeStamp) . sigCT
signatureExpirationTime :: SignaturePayload -> Maybe UTCTime
signatureExpirationTime sig =
addDurationToTime <$>
signatureCreationTime sig <*>
signatureExpirationDuration sig
signatureExpirationDuration :: SignaturePayload -> Maybe ThirtyTwoBitDuration
signatureExpirationDuration sig =
signatureHashedSubpacketsKnown sig >>= firstSignatureExpirationDuration
firstSignatureExpirationDuration :: [SigSubPacket] -> Maybe ThirtyTwoBitDuration
firstSignatureExpirationDuration =
foldr
(\subpacket acc ->
case subpacket of
SigSubPacket _ (SigExpirationTime duration) -> Just duration
_ -> acc)
Nothing
signatureExpirationDurationToUTCTime ::
SomePKPayload -> ThirtyTwoBitDuration -> Maybe UTCTime
signatureExpirationDurationToUTCTime _ (ThirtyTwoBitDuration 0) = Nothing
signatureExpirationDurationToUTCTime pkp duration =
Just $
addDurationToTime
(_timestamp pkp & posixSecondsToUTCTime . realToFrac)
duration
addDurationToTime :: UTCTime -> ThirtyTwoBitDuration -> UTCTime
addDurationToTime baseTime duration =
addUTCTime (fromIntegral (unThirtyTwoBitDuration duration)) baseTime
newestByCreationTime :: [(UTCTime, a)] -> Maybe (UTCTime, a)
newestByCreationTime [] = Nothing
newestByCreationTime xs = Just (maximumBy (comparing fst) xs)
mapMaybeSignatureCreationTime :: [SignaturePayload] -> [(UTCTime, SignaturePayload)]
mapMaybeSignatureCreationTime =
foldr
(\sig acc ->
case signatureCreationTime sig of
Just ct -> (ct, sig) : acc
Nothing -> acc)
[]
selfCertificationGroupActiveAt :: UTCTime -> [SignaturePayload] -> Bool
selfCertificationGroupActiveAt ct sigs =
maybe False (const True) (latestActiveSelfCertificationAt ct sigs)
latestActiveSelfCertificationAt :: UTCTime -> [SignaturePayload] -> Maybe SignaturePayload
latestActiveSelfCertificationAt ct sigs =
case latestKnownSelfCertification of
Nothing -> Nothing
Just certification ->
if signatureEffectiveAt ct certification &&
not (any (\revocation -> revokesCertificationAt ct revocation certification) certRevocations)
then Just certification
else Nothing
where
latestKnownSelfCertification =
snd <$> newestByCreationTime (mapMaybeSignatureCreationTime knownCertifications)
knownCertifications =
filter (\sig -> isCertificationSig sig && signatureCreatedAtOrBefore ct sig) sigs
certRevocations =
filter (\sig -> isCertRevocationForTime ct sig) sigs
latestEffectivePreferenceCarrierAt :: UTCTime -> TKUnknown -> Maybe SignaturePayload
latestEffectivePreferenceCarrierAt ct tk =
snd <$> newestByCreationTime (mapMaybeSignatureCreationTime candidates)
where
primaryKey = tk ^. tkuKey . _1
directKeySigs =
filter
(\sig -> isDirectKeySelfSigFor primaryKey sig && signatureEffectiveAt ct sig)
(tk ^. tkuRevs)
uidSelfCerts =
mapMaybe
(latestActiveSelfCertificationAt ct . filter (isSelfSignatureFor primaryKey) . snd)
(tk ^. tkuUIDs)
uatSelfCerts =
mapMaybe
(latestActiveSelfCertificationAt ct . filter (isSelfSignatureFor primaryKey) . snd)
(tk ^. tkuUAts)
candidates = directKeySigs ++ uidSelfCerts ++ uatSelfCerts
preferencePayloadsFromSignature :: SignaturePayload -> [SigSubPacketPayload]
preferencePayloadsFromSignature =
map
(\(SigSubPacket _ payload) -> payload) .
filter isPreferenceSubpacket .
signatureHashedSubpackets
signatureHashedSubpackets :: SignaturePayload -> [SigSubPacket]
signatureHashedSubpackets sig =
maybe [] id (signatureHashedSubpacketsKnown sig)
isPreferenceSubpacket :: SigSubPacket -> Bool
isPreferenceSubpacket (SigSubPacket _ (PreferredSymmetricAlgorithms _)) = True
isPreferenceSubpacket (SigSubPacket _ (PreferredHashAlgorithms _)) = True
isPreferenceSubpacket (SigSubPacket _ (PreferredCompressionAlgorithms _)) = True
isPreferenceSubpacket (SigSubPacket _ (KeyServerPreferences _)) = True
isPreferenceSubpacket (SigSubPacket _ (PreferredKeyServer _)) = True
isPreferenceSubpacket (SigSubPacket _ (Features _)) = True
isPreferenceSubpacket (SigSubPacket _ (OtherSigSub subpacketType _)) =
subpacketType == 39
isPreferenceSubpacket _ = False
revokesCertificationAt :: UTCTime -> SignaturePayload -> SignaturePayload -> Bool
revokesCertificationAt ct revocation certification =
signatureEffectiveAt ct revocation &&
case (signatureCreationTime certification, signatureCreationTime revocation) of
(Just certificationTime, Just revocationTime) -> certificationTime < revocationTime
_ -> False
isCertRevocationForTime :: UTCTime -> SignaturePayload -> Bool
isCertRevocationForTime ct sig =
sigType sig == Just CertRevocationSig &&
signatureCreatedAtOrBefore ct sig
isCertificationSig :: SignaturePayload -> Bool
isCertificationSig sig =
sigType sig `elem` [Just GenericCert, Just PersonaCert, Just CasualCert, Just PositiveCert]
isDirectKeySelfSigFor :: SomePKPayload -> SignaturePayload -> Bool
isDirectKeySelfSigFor pkp sig =
sigType sig == Just SignatureDirectlyOnAKey && isSelfSignatureFor pkp sig
isSelfCertificationFor :: SomePKPayload -> SignaturePayload -> Bool
isSelfCertificationFor pkp sig =
isCertificationSig sig && isSelfSignatureFor pkp sig
isSelfSignatureFor :: SomePKPayload -> SignaturePayload -> Bool
isSelfSignatureFor pkp sig =
(((== fingerprint pkp) <$> issuerFP (SignaturePkt sig)) == Just True) ||
(((==) <$> issuer (SignaturePkt sig) <*> hush (eightOctetKeyID pkp)) == Just True)
getKeyExpirationTimesFromSignature :: SignaturePayload -> [ThirtyTwoBitDuration]
getKeyExpirationTimesFromSignature sig =
map (\(SigSubPacket _ (KeyExpirationTime x)) -> x) $
filter isKET (signatureHashedSubpackets sig)