packages feed

hOpenPGP-3.7.2: 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 AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Codec.Encryption.OpenPGP.Expirations
    ( KeyState (..)
    , keyStateAt
    , effectiveKeyPreferencesAt
    , effectiveUIDPreferencesAt
    , effectiveKeyPreferencesAtTimestamp
    , effectiveUIDPreferencesAtTimestamp
    , isTKTimeValid
    , isPKTimeValidWithSelfSignatures
    , getKeyExpirationTimesFromSignature
    , isCertificationSig
    , signatureCreationTime
    , signatureExpirationTime
    , signatureExpirationDuration
    , firstSignatureExpirationDuration
    , signatureEffectiveAt
    , addDurationToTime
    , newestByCreationTime
    , keyFlagsFromSignature
    , effectiveKeyFlagsAt
    , effectiveSubkeyFlagsAt
    , effectiveFeaturesAt
    ) where

import Control.Error.Util (hush)
import Control.Lens ((&), (^.))
import Data.List (find, maximumBy)
import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)
import Data.Ord (comparing)
import Data.Set (Set)
import qualified Data.Set as Set
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

{- | Type class for extracting PK payload from subkeys.
This is needed because TKKeyPkt k varies by key kind.
-}
class TKSubkeyPKPayload (k :: TKKind) where
    tkSubkeyPKPayload :: TKKeyPkt k -> SomePKPayload

instance TKSubkeyPKPayload 'PublicTK where
    tkSubkeyPKPayload = keyPktPKPayload

instance TKSubkeyPKPayload 'SecretTK where
    tkSubkeyPKPayload = keyPktPKPayload

instance TKSubkeyPKPayload 'MixedTK where
    tkSubkeyPKPayload (SomeKeyPkt kp) = keyPktPKPayload kp

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 :: TKPrimaryPKPayload k => UTCTime -> TK k -> Bool
isTKTimeValid ct = keyStateValid . keyStateAt ct

keyStateAt :: TKPrimaryPKPayload k => UTCTime -> TK k -> KeyState
keyStateAt ct tk =
    baseState
        { keyStateValid =
            keyStateValid baseState && bindingStateAllowsValidation
        }
  where
    baseState =
        keyStateFromSelfSignaturesAt
            ct
            (tkPrimaryPKPayload tk)
            relevantSelfSignatures
    relevantSelfSignatures =
        concat
            [ filter (isDirectKeySelfSigFor primaryKey) (tk ^. tkDirectKeySigs)
            , filter
                (isSelfCertificationFor primaryKey)
                (concatMap snd (tk ^. tkUIDs))
            , filter
                (isSelfCertificationFor primaryKey)
                (concatMap snd (tk ^. tkUAts))
            ]
    selfCertificationGroups =
        concat
            [ map (filter (isSelfSignatureFor primaryKey) . snd) (tk ^. tkUIDs)
            , map (filter (isSelfSignatureFor primaryKey) . snd) (tk ^. tkUAts)
            ]
    hasAnySelfCertification = any (any isCertificationSig) selfCertificationGroups
    hasAnyActiveSelfCertification =
        any (selfCertificationGroupActiveAt ct) selfCertificationGroups
    bindingStateAllowsValidation =
        not hasAnySelfCertification || hasAnyActiveSelfCertification
    primaryKey = tkPrimaryPKPayload tk

effectiveKeyPreferencesAt
    :: TKPrimaryPKPayload k
    => UTCTime -> TK k -> 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
    :: TKPrimaryPKPayload k
    => UTCTime -> Text -> TK k -> Maybe [SigSubPacketPayload]
effectiveUIDPreferencesAt ct uid tk
    | not (keyStateValid (keyStateAt ct tk)) = Nothing
    | otherwise = do
        sigs <- lookup uid (tk ^. tkUIDs)
        cert <-
            latestActiveSelfCertificationAt
                ct
                (filter (isSelfSignatureFor primaryKey) sigs)
        let prefs = preferencePayloadsFromSignature cert
        if null prefs
            then Nothing
            else Just prefs
  where
    primaryKey = tkPrimaryPKPayload tk

effectiveKeyPreferencesAtTimestamp
    :: TKPrimaryPKPayload k
    => ThirtyTwoBitTimeStamp -> TK k -> Maybe [SigSubPacketPayload]
effectiveKeyPreferencesAtTimestamp ts =
    effectiveKeyPreferencesAt
        (posixSecondsToUTCTime (realToFrac (unThirtyTwoBitTimeStamp ts)))

effectiveUIDPreferencesAtTimestamp
    :: TKPrimaryPKPayload k
    => ThirtyTwoBitTimeStamp
    -> Text
    -> TK k
    -> 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
    :: TKPrimaryPKPayload k => UTCTime -> TK k -> Maybe SignaturePayload
latestEffectivePreferenceCarrierAt ct tk =
    snd
        <$> newestByCreationTime (mapMaybeSignatureCreationTime candidates)
  where
    primaryKey = tkPrimaryPKPayload tk
    directKeySigs =
        filter
            ( \sig ->
                isDirectKeySelfSigFor primaryKey sig
                    && signatureEffectiveAt ct sig
            )
            (tk ^. tkDirectKeySigs)
    uidSelfCerts =
        mapMaybe
            ( latestActiveSelfCertificationAt ct
                . filter (isSelfSignatureFor primaryKey)
                . snd
            )
            (tk ^. tkUIDs)
    uatSelfCerts =
        mapMaybe
            ( latestActiveSelfCertificationAt ct
                . filter (isSelfSignatureFor primaryKey)
                . snd
            )
            (tk ^. tkUAts)
    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 _ (PreferredAEADCiphersuites _)) = 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 DirectKeySignature
        && 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 =
    mapMaybe
        ( \(SigSubPacket _ payload) ->
            case payload of
                KeyExpirationTime x -> Just x
                _ -> Nothing
        )
        (signatureHashedSubpackets sig)

-- | Extract KeyFlags from a single signature's hashed subpackets.
keyFlagsFromSignature :: SignaturePayload -> Maybe (Set KeyFlag)
keyFlagsFromSignature sig =
    case signatureHashedSubpacketsKnown sig of
        Nothing -> Nothing
        Just subpackets ->
            foldr
                ( \(SigSubPacket _ payload) acc ->
                    case payload of
                        KeyFlags flags -> Just (maybe flags (Set.union flags) acc)
                        _ -> acc
                )
                Nothing
                subpackets

{- | Get effective key flags for the primary key at a given timestamp.
Finds the latest effective self-signature (direct key sig, UID self-cert, or UAT self-cert)
and extracts KeyFlags from its hashed subpackets.
Returns Nothing if no effective self-signature exists or if no KeyFlags subpacket is present.
-}
effectiveKeyFlagsAt
    :: TKPrimaryPKPayload k
    => UTCTime -> TK k -> Maybe (Set KeyFlag)
effectiveKeyFlagsAt ct tk = do
    sig <- latestEffectivePreferenceCarrierAt ct tk
    keyFlagsFromSignature sig

{- | Get effective key flags for a subkey at a given timestamp.
Finds the subkey by fingerprint, then finds the latest effective
subkey binding signature and extracts KeyFlags from its hashed subpackets.
-}
effectiveSubkeyFlagsAt
    :: forall k
     . (TKPrimaryPKPayload k, TKSubkeyPKPayload k)
    => UTCTime -> TK k -> Fingerprint -> Maybe (Set KeyFlag)
effectiveSubkeyFlagsAt ct tk fp = do
    (_, bindingSigs) <-
        find
            (\(kp, _) -> fingerprint (tkSubkeyPKPayload @k kp) == fp)
            (tk ^. tkSubs)
    bindingSig <-
        latestEffectiveSubkeyBindingSignatureAt ct bindingSigs
    keyFlagsFromSignature bindingSig

{- | Get effective features for the primary key at a given timestamp.
Finds the latest effective self-signature and extracts Features from its hashed subpackets.
-}
effectiveFeaturesAt
    :: TKPrimaryPKPayload k
    => UTCTime -> TK k -> Maybe (Set FeatureFlag)
effectiveFeaturesAt ct tk = do
    sig <- latestEffectivePreferenceCarrierAt ct tk
    featuresFromSignature sig

-- | Extract Features from a single signature's hashed subpackets.
featuresFromSignature
    :: SignaturePayload -> Maybe (Set FeatureFlag)
featuresFromSignature sig =
    case signatureHashedSubpacketsKnown sig of
        Nothing -> Nothing
        Just subpackets ->
            foldr
                ( \(SigSubPacket _ payload) acc ->
                    case payload of
                        Features flags -> Just (maybe flags (Set.union flags) acc)
                        _ -> acc
                )
                Nothing
                subpackets

{- | Find the latest effective subkey binding signature at a given timestamp.
This is similar to the function in Encrypt.hs but uses UTCTime instead of ThirtyTwoBitTimeStamp.
-}
latestEffectiveSubkeyBindingSignatureAt
    :: UTCTime -> [SignaturePayload] -> Maybe SignaturePayload
latestEffectiveSubkeyBindingSignatureAt ct sigs =
    case filter (isEffectiveSubkeyBindingSignatureAt ct) sigs of
        [] -> Nothing
        candidates ->
            Just
                (maximumBy (comparing signatureCreationTimeOrZero) candidates)

-- | Check if a signature is an effective subkey binding signature at the given time.
isEffectiveSubkeyBindingSignatureAt
    :: UTCTime -> SignaturePayload -> Bool
isEffectiveSubkeyBindingSignatureAt ct sig =
    isSubkeyBindingSig sig
        && maybe
            False
            ( \created ->
                created <= ct
                    && maybe
                        True
                        ( \duration ->
                            if unThirtyTwoBitDuration duration == 0
                                then True
                                else
                                    ct
                                        < addUTCTime
                                            (fromIntegral (unThirtyTwoBitDuration duration))
                                            created
                        )
                        (signatureExpirationDuration sig)
            )
            (signatureCreationTime sig)

-- | Check if a signature is a subkey binding signature.
isSubkeyBindingSig :: SignaturePayload -> Bool
isSubkeyBindingSig sig = sigType sig == Just SubkeyBindingSig

-- | Get signature creation time or zero if not present.
signatureCreationTimeOrZero :: SignaturePayload -> UTCTime
signatureCreationTimeOrZero sig =
    fromMaybe (posixSecondsToUTCTime 0) (signatureCreationTime sig)