packages feed

hOpenPGP-3.0.0: Codec/Encryption/OpenPGP/Subpackets.hs

-- Subpackets.hs: Type-safe subpacket builders with phantom types
-- Copyright © 2012-2026  Clint Adams
-- This software is released under the terms of the Expat license.
-- (See the LICENSE file).

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

module Codec.Encryption.OpenPGP.Subpackets
  ( -- * Safe subpacket construction
    SafeSubpacket
  , mkSafeSubpacket
  , safePayload
  , safeSubpacket

    -- * Critical subpacket handling
  , CriticalSubpacket
  , mkCritical
  , canBeCritical

    -- * Text normalization mode
  , TextNormalizationMode(..)

    -- * Builder API for signature composition
  , SigBuilder
  , LegalSubpacket(..)
  , legalSubpacket
  , singleLegalSub
  , consLegalSub
  , listToLegalSubs
  , sbSigType
  , sbPubKeyAlgo
  , sbHashAlgo
  , sbHashedSubs
  , sbUnhashedSubs
  , sbSalt
  , sbTextNormMode
  , buildSigV4
  , buildSigV6
  , sigBuilderInit
  , sigBuilderInitTyped
  , sigBuilderInitRuntime
  , sigBuilderInitV6
  , sigBuilderInitV6Typed
  , sigBuilderInitV6Runtime
  , addHashedSubs
  , addUnhashedSubs
  , KnownPubKeyAlgorithm(..)

    -- * Private key wrapper (algorithm-specific)
  , PrivateKeyFor(..)

    -- * Subpacket list utilities
  , HashedSubpackets
  , UnhashedSubpackets
  , consHashedSub
  , singleHashedSub
  , singleUnhashedSub
  , listToHashedSubs
  , listToUnhashedSubs
  ) where

import Data.Kind (Type)
import Data.List.NonEmpty (NonEmpty)
import Data.Proxy (Proxy(..))
import Data.Word (Word16)

import qualified Crypto.PubKey.DSA as DSA
import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
import qualified Crypto.PubKey.Ed25519 as Ed25519
import qualified Crypto.PubKey.Ed448 as Ed448
import qualified Crypto.PubKey.RSA.Types as RSATypes

import Codec.Encryption.OpenPGP.Types
  ( Hashed
  , Unhashed
  , V4Sig
  , V6Sig
  , Fingerprint
  , EightOctetKeyId
  , ThirtyTwoBitTimeStamp
  , MPI
  , SigSubPacket(..)
  , SigSubPacketPayload(..)
  , SignaturePayload(..)
  , SignatureSalt
  , SigType
  , HashAlgorithm
  , SubpacketList(..)
  )
import Codec.Encryption.OpenPGP.Types.Internal.Base
  ( PubKeyAlgorithm(..)
  , IssuerFingerprintVersion(..)
  )
import Codec.Encryption.OpenPGP.Policy
  ( HashAlgoAllowedFor
  , OpenPGPRFC
  , OpenPGPRFCW(..)
  , SomeOpenPGPRFCW(..)
  , HashAlgorithmW(..)
  , SomeHashAlgorithmW(..)
  , promoteOpenPGPRFC
  , promoteHashAlgorithm
  , demoteHashAlgorithmW
  , policyForRFC
  , policyGenerationDeprecations
  , deprecatedHashAlgorithms
  )

-- | Type alias for readability: hashed subpacket list
type HashedSubpackets v = SubpacketList Hashed v

-- | Type alias for readability: unhashed subpacket list
type UnhashedSubpackets v = SubpacketList Unhashed v

-- | A subpacket that has passed basic safety checks
-- (does not guarantee it's critical-safe; use CriticalSubpacket for that)
newtype SafeSubpacket = SafeSubpacket SigSubPacket
  deriving (Eq, Ord, Show)

-- | Create a safe subpacket from a payload and criticality flag
-- Basic validation that the payload is well-formed
mkSafeSubpacket :: Bool -> SigSubPacketPayload -> Either String SafeSubpacket
mkSafeSubpacket crit payload = do
  -- Validate criticality constraints: some subpackets should never be critical
  if crit && not (canBeCritical payload)
    then Left $ "Subpacket type cannot be marked critical: " ++ show payload
    else Right (SafeSubpacket (SigSubPacket crit payload))

-- | Extract the payload from a safe subpacket
safePayload :: SafeSubpacket -> SigSubPacketPayload
safePayload (SafeSubpacket (SigSubPacket _ payload)) = payload

-- | Extract the underlying SigSubPacket from a safe subpacket
safeSubpacket :: SafeSubpacket -> SigSubPacket
safeSubpacket (SafeSubpacket ssp) = ssp

-- | A subpacket that is guaranteed to be both well-formed AND can be safely marked critical
-- Used to prevent accidentally marking non-critical-safe types as critical
newtype CriticalSubpacket = CriticalSubpacket SigSubPacket
  deriving (Eq, Ord, Show)

-- | Create a critical subpacket only if the payload type allows it
-- RFC9580 section 5.2.3.5: only certain subpacket types can be marked critical
mkCritical :: SigSubPacketPayload -> Either String CriticalSubpacket
mkCritical payload
  | canBeCritical payload = Right $ CriticalSubpacket (SigSubPacket True payload)
  | otherwise = Left $ "Cannot mark as critical: " ++ payloadType payload
  where
    payloadType (SigCreationTime _) = "SigCreationTime"
    payloadType (SigExpirationTime _) = "SigExpirationTime"
    payloadType (ExportableCertification _) = "ExportableCertification"
    payloadType (TrustSignature {}) = "TrustSignature"
    payloadType (RegularExpression _) = "RegularExpression"
    payloadType (Revocable _) = "Revocable"
    payloadType (KeyExpirationTime _) = "KeyExpirationTime"
    payloadType (PreferredSymmetricAlgorithms _) = "PreferredSymmetricAlgorithms"
    payloadType (RevocationKey {}) = "RevocationKey"
    payloadType (Issuer _) = "Issuer"
    payloadType (NotationData {}) = "NotationData"
    payloadType (PreferredHashAlgorithms _) = "PreferredHashAlgorithms"
    payloadType (PreferredCompressionAlgorithms _) = "PreferredCompressionAlgorithms"
    payloadType (KeyServerPreferences _) = "KeyServerPreferences"
    payloadType (PreferredKeyServer _) = "PreferredKeyServer"
    payloadType (PrimaryUserId _) = "PrimaryUserId"
    payloadType (PolicyURL _) = "PolicyURL"
    payloadType (KeyFlags _) = "KeyFlags"
    payloadType (SignersUserId _) = "SignersUserId"
    payloadType (ReasonForRevocation {}) = "ReasonForRevocation"
    payloadType (Features _) = "Features"
    payloadType (SignatureTarget {}) = "SignatureTarget"
    payloadType (EmbeddedSignature _) = "EmbeddedSignature"
    payloadType (IssuerFingerprint {}) = "IssuerFingerprint"
    payloadType (UserDefinedSigSub _ _) = "UserDefinedSigSub"
    payloadType (OtherSigSub _ _) = "OtherSigSub"

-- | RFC9580 §5.2.3.5 lists which subpacket types can be marked critical
canBeCritical :: SigSubPacketPayload -> Bool
canBeCritical KeyFlags {} = True
canBeCritical IssuerFingerprint {} = True
canBeCritical EmbeddedSignature {} = True
-- Future-proofing: unknown critical types are allowed to be critical
canBeCritical UserDefinedSigSub {} = True
canBeCritical OtherSigSub {} = True
canBeCritical _ = False

-- | Wrapper GADT for algorithm-specific private keys
-- Encodes the algorithm at the type level to ensure type-safe key/builder matching
data PrivateKeyFor (algo :: PubKeyAlgorithm) where
  RSAPrivateKey :: RSATypes.PrivateKey -> PrivateKeyFor 'RSA
  Ed25519PrivateKey :: Ed25519.SecretKey -> PrivateKeyFor 'Ed25519
  Ed448PrivateKey :: Ed448.SecretKey -> PrivateKeyFor 'Ed448
  DSAPrivateKey :: DSA.PrivateKey -> PrivateKeyFor 'DSA
  ECDSAPrivateKey :: ECDSA.PrivateKey -> PrivateKeyFor 'ECDSA

class KnownPubKeyAlgorithm (algo :: PubKeyAlgorithm) where
  demotePubKeyAlgorithmT :: Proxy algo -> PubKeyAlgorithm

instance KnownPubKeyAlgorithm 'RSA where
  demotePubKeyAlgorithmT _ = RSA

instance KnownPubKeyAlgorithm 'DSA where
  demotePubKeyAlgorithmT _ = DSA

instance KnownPubKeyAlgorithm 'ECDSA where
  demotePubKeyAlgorithmT _ = ECDSA

instance KnownPubKeyAlgorithm 'EdDSA where
  demotePubKeyAlgorithmT _ = EdDSA

instance KnownPubKeyAlgorithm 'Ed25519 where
  demotePubKeyAlgorithmT _ = Ed25519

instance KnownPubKeyAlgorithm 'Ed448 where
  demotePubKeyAlgorithmT _ = Ed448

-- | Legal subpackets encoded by placement and signature-version constraints.
-- This expands compile-time legality beyond bare hashed/unhashed staging.
data LegalSubpacket (h :: Type) (v :: Type) where
  -- RFC9580: signature creation time is a hashed subpacket.
  LegalSigCreationTime :: ThirtyTwoBitTimeStamp -> LegalSubpacket Hashed v
  -- v4 signatures use an issuer fingerprint subpacket with version marker 4 in hashed area.
  LegalIssuerFingerprintV4 :: Fingerprint -> LegalSubpacket Hashed V4Sig
  -- v6 signatures use an issuer fingerprint subpacket with version marker 6 in hashed area.
  LegalIssuerFingerprintV6 :: Fingerprint -> LegalSubpacket Hashed V6Sig
  -- Legacy issuer key ID subpacket is accepted only in v4 and only unhashed.
  LegalIssuerV4 :: EightOctetKeyId -> LegalSubpacket Unhashed V4Sig

legalSubpacket :: LegalSubpacket h v -> SigSubPacket
legalSubpacket (LegalSigCreationTime ts) = SigSubPacket False (SigCreationTime ts)
legalSubpacket (LegalIssuerFingerprintV4 fp) = SigSubPacket False (IssuerFingerprint IssuerFingerprintV4 fp)
legalSubpacket (LegalIssuerFingerprintV6 fp) = SigSubPacket False (IssuerFingerprint IssuerFingerprintV6 fp)
legalSubpacket (LegalIssuerV4 keyId) = SigSubPacket False (Issuer keyId)

singleLegalSub :: LegalSubpacket h v -> SubpacketList h v
singleLegalSub = SubpacketList . (: []) . legalSubpacket

consLegalSub :: LegalSubpacket h v -> SubpacketList h v -> SubpacketList h v
consLegalSub legal (SubpacketList sps) = SubpacketList (legalSubpacket legal : sps)

listToLegalSubs :: [LegalSubpacket h v] -> SubpacketList h v
listToLegalSubs = SubpacketList . fmap legalSubpacket

-- | Construct a single hashed subpacket into a hashed subpacket list
singleHashedSub :: SigSubPacket -> HashedSubpackets v
singleHashedSub sp = SubpacketList [sp]

-- | Construct a single unhashed subpacket into an unhashed subpacket list
singleUnhashedSub :: SigSubPacket -> UnhashedSubpackets v
singleUnhashedSub sp = SubpacketList [sp]

-- | Prepend a subpacket to a hashed subpacket list
consHashedSub :: SigSubPacket -> HashedSubpackets v -> HashedSubpackets v
consHashedSub sp (SubpacketList sps) = SubpacketList (sp : sps)

-- | Controls how text payload is normalized for CanonicalTextSig.
--
-- RFC 9580 §5.2.1.2 requires only CRLF normalization for type 0x01
-- signatures.  Trailing-whitespace stripping is only mandated by
-- §7 (Cleartext Signature Framework).  Use 'CleartextCompat' for
-- GnuPG-compatible behaviour when producing cleartext-armored
-- signatures; use 'RFC9580Strict' for inline text signatures.
data TextNormalizationMode
  = RFC9580Strict
    -- ^ CRLF normalization only; no trailing-whitespace stripping.
    --   Correct for inline type 0x01 document signatures.
  | CleartextCompat
    -- ^ CRLF normalization *plus* per-line trailing-whitespace stripping.
    --   Required by the Cleartext Signature Framework (RFC 9580 §7).
  deriving (Eq, Show)

-- | Staged signature builder that enforces completion order
-- 
-- Usage pattern:
--   builder <- sigBuilderInit SigTypeBinary RSA SHA256
--   builder' <- addHashedSubs hashedList builder
--   finalPayload <- buildSigV4 sigMPIs (addUnhashedSubs unhashedList builder')
data SigBuilder (hashedness :: Type) (v :: Type) (algo :: PubKeyAlgorithm) = SigBuilder
  { sbSigType :: SigType
  , sbHashAlgo :: HashAlgorithm
  , sbHashedSubs :: [SigSubPacket]
  , sbUnhashedSubs :: [SigSubPacket]
  , sbSalt :: BuilderSalt v
  , sbTextNormMode :: TextNormalizationMode
    -- ^ Normalization mode for CanonicalTextSig payloads.
    --   Defaults to 'CleartextCompat' for backward compatibility.
  }

type family BuilderSalt (v :: Type) where
  BuilderSalt V4Sig = ()
  BuilderSalt V6Sig = SignatureSalt

sbPubKeyAlgo :: forall hashedness v algo. KnownPubKeyAlgorithm algo => SigBuilder hashedness v algo -> PubKeyAlgorithm
sbPubKeyAlgo _ = demotePubKeyAlgorithmT (Proxy @algo)

-- | Initialize a builder for a v4 signature
-- Starts with no subpackets and must call addHashedSubs and addUnhashedSubs
sigBuilderInit ::
     forall algo
   . KnownPubKeyAlgorithm algo
  => SigType
  -> HashAlgorithm
  -> SigBuilder Hashed V4Sig algo
sigBuilderInit st ha = SigBuilder
  { sbSigType = st
  , sbHashAlgo = ha
  , sbHashedSubs = []
  , sbUnhashedSubs = []
  , sbSalt = ()
  , sbTextNormMode = CleartextCompat
  }

-- | Initialize a builder for a v4 signature with compile-time RFC/hash policy enforcement.
sigBuilderInitTyped ::
     forall algo rfc h
   . (KnownPubKeyAlgorithm algo, HashAlgoAllowedFor rfc h)
  => OpenPGPRFCW rfc
  -> SigType
  -> HashAlgorithmW h
  -> SigBuilder Hashed V4Sig algo
sigBuilderInitTyped _ st hashW =
  sigBuilderInit @algo st (demoteHashAlgorithmW hashW)

-- | Initialize a v4 builder from runtime RFC/hash inputs with policy validation
-- and witness promotion at the API boundary.
sigBuilderInitRuntime ::
     forall algo
   . KnownPubKeyAlgorithm algo
  => OpenPGPRFC
  -> SigType
  -> HashAlgorithm
  -> Either String (SigBuilder Hashed V4Sig algo)
sigBuilderInitRuntime rfc st ha =
  withGenerationHashWitness
    rfc
    ha
    (\rfcW hashW -> sigBuilderInitTyped @algo rfcW st hashW)

-- | Initialize a builder for a v6 signature
sigBuilderInitV6 ::
     forall algo
   . KnownPubKeyAlgorithm algo
  => SigType
  -> HashAlgorithm
  -> SignatureSalt
  -> SigBuilder Hashed V6Sig algo
sigBuilderInitV6 st ha salt = SigBuilder
  { sbSigType = st
  , sbHashAlgo = ha
  , sbHashedSubs = []
  , sbUnhashedSubs = []
  , sbSalt = salt
  , sbTextNormMode = CleartextCompat
  }

-- | Initialize a builder for a v6 signature with compile-time RFC/hash policy enforcement.
sigBuilderInitV6Typed ::
     forall algo rfc h
   . (KnownPubKeyAlgorithm algo, HashAlgoAllowedFor rfc h)
  => OpenPGPRFCW rfc
  -> SigType
  -> HashAlgorithmW h
  -> SignatureSalt
  -> SigBuilder Hashed V6Sig algo
sigBuilderInitV6Typed _ st hashW salt =
  sigBuilderInitV6 @algo st (demoteHashAlgorithmW hashW) salt

-- | Initialize a v6 builder from runtime RFC/hash inputs with policy validation
-- and witness promotion at the API boundary.
sigBuilderInitV6Runtime ::
     forall algo
   . KnownPubKeyAlgorithm algo
  => OpenPGPRFC
  -> SigType
  -> HashAlgorithm
  -> SignatureSalt
  -> Either String (SigBuilder Hashed V6Sig algo)
sigBuilderInitV6Runtime rfc st ha salt =
  withGenerationHashWitness
    rfc
    ha
    (\rfcW hashW -> sigBuilderInitV6Typed @algo rfcW st hashW salt)

withGenerationHashWitness ::
     OpenPGPRFC
  -> HashAlgorithm
  -> (forall rfc h. HashAlgoAllowedFor rfc h => OpenPGPRFCW rfc -> HashAlgorithmW h -> a)
  -> Either String a
withGenerationHashWitness rfc ha mk
  | ha `elem` deprecatedHashAlgorithms (policyGenerationDeprecations (policyForRFC rfc)) =
      Left (hashPolicyDisallowedMessage rfc ha)
  | otherwise =
      case promoteHashAlgorithm ha of
        Nothing ->
          Left (hashNotTypedBuilderMessage ha)
        Just (SomeHashAlgorithmW hashW) ->
          case promoteOpenPGPRFC rfc of
            SomeOpenPGPRFCW RFC2440W ->
              Right $
                case hashW of
                  DeprecatedMD5W -> mk RFC2440W DeprecatedMD5W
                  SHA1W -> mk RFC2440W SHA1W
                  RIPEMD160W -> mk RFC2440W RIPEMD160W
                  SHA256W -> mk RFC2440W SHA256W
                  SHA384W -> mk RFC2440W SHA384W
                  SHA512W -> mk RFC2440W SHA512W
                  SHA224W -> mk RFC2440W SHA224W
                  SHA3_256W -> mk RFC2440W SHA3_256W
                  SHA3_512W -> mk RFC2440W SHA3_512W
            SomeOpenPGPRFCW RFC4880W ->
              case hashW of
                SHA1W -> Right (mk RFC4880W SHA1W)
                RIPEMD160W -> Right (mk RFC4880W RIPEMD160W)
                SHA256W -> Right (mk RFC4880W SHA256W)
                SHA384W -> Right (mk RFC4880W SHA384W)
                SHA512W -> Right (mk RFC4880W SHA512W)
                SHA224W -> Right (mk RFC4880W SHA224W)
                SHA3_256W -> Right (mk RFC4880W SHA3_256W)
                SHA3_512W -> Right (mk RFC4880W SHA3_512W)
                DeprecatedMD5W ->
                  Left (hashPolicyDisallowedMessage rfc ha)
            SomeOpenPGPRFCW RFC9580W ->
              case hashW of
                SHA256W -> Right (mk RFC9580W SHA256W)
                SHA384W -> Right (mk RFC9580W SHA384W)
                SHA512W -> Right (mk RFC9580W SHA512W)
                SHA224W -> Right (mk RFC9580W SHA224W)
                SHA3_256W -> Right (mk RFC9580W SHA3_256W)
                SHA3_512W -> Right (mk RFC9580W SHA3_512W)
                DeprecatedMD5W ->
                  Left (hashPolicyDisallowedMessage rfc ha)
                SHA1W ->
                  Left (hashPolicyDisallowedMessage rfc ha)
                RIPEMD160W ->
                  Left (hashPolicyDisallowedMessage rfc ha)

hashPolicyDisallowedMessage :: OpenPGPRFC -> HashAlgorithm -> String
hashPolicyDisallowedMessage rfc ha =
  "signature hash algorithm disallowed by RFC policy (" ++ show rfc ++ "): " ++ show ha

hashNotTypedBuilderMessage :: HashAlgorithm -> String
hashNotTypedBuilderMessage ha =
  "signature hash algorithm is not supported by typed builder API: " ++ show ha

-- | Add hashed subpackets to a builder
-- Must be called before addUnhashedSubs
addHashedSubs :: HashedSubpackets v -> SigBuilder Hashed v algo -> SigBuilder Unhashed v algo
addHashedSubs (SubpacketList sps) builder = builder { sbHashedSubs = sps }

-- | Add unhashed subpackets to a builder
-- Must be called after addHashedSubs
addUnhashedSubs :: UnhashedSubpackets v -> SigBuilder Unhashed v algo -> SigBuilder Unhashed v algo
addUnhashedSubs (SubpacketList sps) builder = builder { sbUnhashedSubs = sps }

-- | Build a v4 signature from a completed builder and MPI values
buildSigV4 ::
     KnownPubKeyAlgorithm algo
  => SigBuilder Unhashed V4Sig algo
  -> Word16
  -> NonEmpty MPI
  -> SignaturePayload
buildSigV4 builder hashLeft mpis = SigV4
  (sbSigType builder)
  (sbPubKeyAlgo builder)
  (sbHashAlgo builder)
  (sbHashedSubs builder)
  (sbUnhashedSubs builder)
  hashLeft
  mpis

-- | Build a v6 signature from a completed builder and MPI values
buildSigV6 ::
     KnownPubKeyAlgorithm algo
  => SigBuilder Unhashed V6Sig algo
  -> Word16
  -> NonEmpty MPI
  -> SignaturePayload
buildSigV6 builder hashLeft mpis =
  SigV6
    (sbSigType builder)
    (sbPubKeyAlgo builder)
    (sbHashAlgo builder)
    (sbSalt builder)
    (sbHashedSubs builder)
    (sbUnhashedSubs builder)
    hashLeft
    mpis

-- | Convert a list to a hashed subpacket list
-- Used to bridge between list-based and phantom-typed APIs
listToHashedSubs :: [SigSubPacket] -> HashedSubpackets v
listToHashedSubs sps = SubpacketList sps

-- | Convert a list to an unhashed subpacket list
-- Used to bridge between list-based and phantom-typed APIs
listToUnhashedSubs :: [SigSubPacket] -> UnhashedSubpackets v
listToUnhashedSubs sps = SubpacketList sps