hopenpgp-tools 0.25 → 0.25.1
raw patch · 24 files changed
+2590/−2337 lines, 24 filesdep ~binarydep ~conduitdep ~fgl
Dependency ranges changed: binary, conduit, fgl, hOpenPGP, optparse-applicative, prettyprinter
Files
- HOpenPGP/Tools/Armor.hs +0/−38
- HOpenPGP/Tools/Common.hs +0/−230
- HOpenPGP/Tools/Common/Armor.hs +39/−0
- HOpenPGP/Tools/Common/Common.hs +242/−0
- HOpenPGP/Tools/Common/HKP.hs +126/−0
- HOpenPGP/Tools/Common/Lexer.x +175/−0
- HOpenPGP/Tools/Common/Parser.y +229/−0
- HOpenPGP/Tools/Common/TKUtils.hs +94/−0
- HOpenPGP/Tools/Common/WKD.hs +183/−0
- HOpenPGP/Tools/HKP.hs +0/−126
- HOpenPGP/Tools/Hokey/Canonicalize.hs +57/−0
- HOpenPGP/Tools/Hokey/Fetch.hs +50/−0
- HOpenPGP/Tools/Hokey/InjectSSHAgent.hs +374/−0
- HOpenPGP/Tools/Hokey/Lint.hs +710/−0
- HOpenPGP/Tools/Hokey/Options.hs +146/−0
- HOpenPGP/Tools/Lexer.x +0/−175
- HOpenPGP/Tools/Parser.y +0/−229
- HOpenPGP/Tools/TKUtils.hs +0/−94
- HOpenPGP/Tools/WKD.hs +0/−183
- hkt.hs +23/−8
- hokey.hs +86/−1218
- hop.hs +26/−11
- hopenpgp-tools.cabal +26/−21
- hot.hs +4/−4
− HOpenPGP/Tools/Armor.hs
@@ -1,38 +0,0 @@-{-# LANGUAGE RecordWildCards #-}---- Armor.hs: hOpenPGP-tools common ASCII de-Armor function--- Copyright © 2012-2023 Clint Adams------ vim: softtabstop=4:shiftwidth=4:expandtab------ This program is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as--- published by the Free Software Foundation, either version 3 of the--- License, or (at your option) any later version.------ This program is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with this program. If not, see <http://www.gnu.org/licenses/>.-module HOpenPGP.Tools.Armor- ( doDeArmor- ) where--import qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA-import Codec.Encryption.OpenPGP.ASCIIArmor.Types (Armor(..))-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import Data.Conduit ((.|), runConduitRes)-import qualified Data.Conduit.Binary as CB-import qualified Data.Conduit.List as CL-import System.IO (hPutStrLn, stderr, stdin)--doDeArmor :: IO ()-doDeArmor = do- a <- runConduitRes $ CB.sourceHandle stdin .| CL.consume- case AA.decode (B.concat a) of- Left e -> hPutStrLn stderr $ "Failure to decode ASCII Armor:" ++ e- Right msgs -> BL.putStr $ BL.concat (map (\(Armor _ _ bs) -> bs) msgs)
− HOpenPGP/Tools/Common.hs
@@ -1,230 +0,0 @@--- Common.hs: hOpenPGP-tools common functions--- Copyright © 2012-2026 Clint Adams------ vim: softtabstop=4:shiftwidth=4:expandtab------ This program is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as--- published by the Free Software Foundation, either version 3 of the--- License, or (at your option) any later version.------ This program is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with this program. If not, see <http://www.gnu.org/licenses/>.--module HOpenPGP.Tools.Common- ( banner- , versioner- , warranty- , prependAuto- , keyMatchesFingerprint- , keyMatchesEightOctetKeyId- , keyMatchesExactUIDString- , keyMatchesUIDSubString- , keyMatchesPKPred- -- hmm- , pkpGetPKVersion- , pkpGetPKAlgo- , pkpGetKeysize- , pkpGetTimestamp- , pkpGetFingerprint- , pkpGetEOKI- , tkUsingPKP- , pUsingPKP- , pUsingSP- , tkGetUIDs- , tkGetSubs- , anyOrAll- , anyReader- , oGetTag- , oGetLength- , spGetSigVersion- , spGetSigType- , spGetPKAlgo- , spGetHashAlgo- , spGetSCT- , maybeR- ) where--import Data.Version (showVersion)-import Paths_hopenpgp_tools (version)--import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)-import Codec.Encryption.OpenPGP.SignatureQualities (sigCT)-import Codec.Encryption.OpenPGP.Types-import Control.Lens ((^..))-import Data.Binary (put)-import Data.Binary.Put (runPut)-import qualified Data.ByteString.Lazy as BL-import Data.Data.Lens (biplate)-import Data.Text (Text)-import qualified Data.Text as T-import Options.Applicative.Builder (auto, help, hidden, infoOption, long, short)-import Options.Applicative.Types (Parser, ReadM(..))-import Prettyprinter (Doc, (<+>), hardline, pretty)--import Codec.Encryption.OpenPGP.KeyInfo (pubkeySize)-import Control.Error.Util (hush)-import Control.Monad.Trans.Reader- ( Reader- , ReaderT- , ask- , local- , reader- , runReader- , withReader- )--- hmm ---import Data.Maybe (fromMaybe, mapMaybe)--banner :: String -> Doc ann-{-# INLINE banner #-}-banner name =- pretty name <+>- pretty "(hopenpgp-tools)" <+>- pretty (showVersion version) <>- hardline <> pretty "Copyright (C) 2012-2026 Clint Adams"--warranty :: String -> Doc ann-{-# INLINE warranty #-}-warranty name =- pretty name <+>- pretty "comes with ABSOLUTELY NO WARRANTY." <+>- pretty "This is free software, and you are welcome to redistribute it" <+>- pretty "under certain conditions."--versioner :: String -> Parser (a -> a)-{-# INLINE versioner #-}-versioner name =- infoOption (name ++ " (hopenpgp-tools) " ++ showVersion version) $- long "version" <> short 'V' <> help "Show version information" <> hidden--prependAuto :: Read a => String -> ReadM a-prependAuto s = ReadM (local (s ++) (unReadM auto))--keyMatchesFingerprint :: Bool -> TKUnknown -> Fingerprint -> Bool-keyMatchesFingerprint = keyMatchesPKPred fingerprint--keyMatchesEightOctetKeyId :: Bool -> TKUnknown -> Either String EightOctetKeyId -> Bool -- FIXME: refactor this somehow-keyMatchesEightOctetKeyId = keyMatchesPKPred eightOctetKeyID--keyMatchesExactUIDString :: Text -> TKUnknown -> Bool-keyMatchesExactUIDString uidstr = elem uidstr . map fst . _tkuUIDs--keyMatchesUIDSubString :: Text -> TKUnknown -> Bool-keyMatchesUIDSubString uidstr =- any (T.toLower uidstr `T.isInfixOf`) . map (T.toLower . fst) . _tkuUIDs--keyMatchesPKPred :: Eq a => (SomePKPayload -> a) -> Bool -> TKUnknown -> a -> Bool-keyMatchesPKPred p False = (==) . p . fst . _tkuKey-keyMatchesPKPred p True = \tk v -> elem v (map p (tk ^.. biplate))---- The following should probably be moved elsewhere-tkUsingPKP :: Reader SomePKPayload a -> Reader TKUnknown a-tkUsingPKP = withReader (fst . _tkuKey)--pkpGetPKVersion :: SomePKPayload -> Integer-pkpGetPKVersion t =- if _keyVersion t == DeprecatedV3- then 3- else 4--pkpGetPKAlgo :: SomePKPayload -> Integer-pkpGetPKAlgo = fromIntegral . fromFVal . _pkalgo--pkpGetKeysize :: SomePKPayload -> Integer-pkpGetKeysize = fromIntegral . fromMaybe 0 . hush . pubkeySize . _pubkey--pkpGetTimestamp :: SomePKPayload -> Integer-pkpGetTimestamp = fromIntegral . _timestamp--pkpGetFingerprint :: SomePKPayload -> Fingerprint-pkpGetFingerprint = fingerprint--pkpGetEOKI :: SomePKPayload -> String-pkpGetEOKI = either (const "UNKNOWN") show . eightOctetKeyID--tkGetUIDs :: TKUnknown -> [Text]-tkGetUIDs = map fst . _tkuUIDs--tkGetSubs :: TKUnknown -> [SomePKPayload]-tkGetSubs = mapMaybe (grabPKP . fst) . _tkuSubs- where- grabPKP (PublicSubkeyPkt p) = Just p- grabPKP (SecretSubkeyPkt p _) = Just p- grabPKP _ = Nothing--anyOrAll ::- (Monad m, Monad m1)- => ((a1 -> c) -> a -> ReaderT a m b)- -> (m1 a1 -> c)- -> ReaderT a m b-anyOrAll aa op = ask >>= aa (op . return)--anyReader :: Reader a Bool -> Reader [a] Bool-anyReader p = any (runReader p) `fmap` ask--oGetTag :: Pkt -> Integer-oGetTag = fromIntegral . pktTag--oGetLength :: Pkt -> Integer-oGetLength = fromIntegral . BL.length . runPut . put -- FIXME: this should be a length that makes sense--spGetSigVersion :: Pkt -> Maybe Integer-spGetSigVersion (SignaturePkt s) = Just (sigVersion s)- where- sigVersion SigV3 {} = 3- sigVersion SigV4 {} = 4- sigVersion (SigVOther v _) = fromIntegral v-spGetSigVersion _ = Nothing--spGetSigType :: Pkt -> Maybe Integer-spGetSigType (SignaturePkt s) = fmap (fromIntegral . fromFVal) (sigType s)- -- FIXME: deduplicate this and hOpenPGP .Internal- where- sigType :: SignaturePayload -> Maybe SigType- sigType (SigV3 st _ _ _ _ _ _) = Just st- sigType (SigV4 st _ _ _ _ _ _) = Just st- sigType _ = Nothing -- this includes v2 sigs, which don't seem to be specified in the RFCs but exist in the wild-spGetSigType _ = Nothing--spGetPKAlgo :: Pkt -> Maybe Integer-spGetPKAlgo (SignaturePkt s) = fmap (fromIntegral . fromFVal) (sigPKA s)- where- sigPKA (SigV3 _ _ _ pka _ _ _) = Just pka- sigPKA (SigV4 _ pka _ _ _ _ _) = Just pka- sigPKA _ = Nothing -- this includes v2 sigs, which don't seem to be specified in the RFCs but exist in the wild-spGetPKAlgo _ = Nothing--spGetHashAlgo :: Pkt -> Maybe Integer-spGetHashAlgo (SignaturePkt s) = fmap (fromIntegral . fromFVal) (sigHA s)- where- sigHA (SigV3 _ _ _ _ ha _ _) = Just ha- sigHA (SigV4 _ _ ha _ _ _ _) = Just ha- sigHA _ = Nothing -- this includes v2 sigs, which don't seem to be specified in the RFCs but exist in the wild-spGetHashAlgo _ = Nothing--spGetSCT :: Pkt -> Maybe Integer-spGetSCT (SignaturePkt s) = fmap fromIntegral (sigCT s)--pUsingPKP :: Reader (Maybe SomePKPayload) a -> Reader Pkt a-pUsingPKP = withReader grabPayload- where- grabPayload (SecretKeyPkt p _) = Just p- grabPayload (PublicKeyPkt p) = Just p- grabPayload (SecretSubkeyPkt p _) = Just p- grabPayload (PublicSubkeyPkt p) = Just p- grabPayload _ = Nothing--pUsingSP :: Reader (Maybe SignaturePayload) a -> Reader Pkt a-pUsingSP = withReader grabPayload- where- grabPayload (SignaturePkt s) = Just s- grabPayload _ = Nothing--maybeR :: a -> Reader r a -> Reader (Maybe r) a-maybeR x r = reader (maybe x (runReader r))
+ HOpenPGP/Tools/Common/Armor.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE RecordWildCards #-}++-- Armor.hs: hOpenPGP-tools common ASCII de-Armor function+-- Copyright © 2012-2026 Clint Adams+--+-- vim: softtabstop=4:shiftwidth=4:expandtab+--+-- This program is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as+-- published by the Free Software Foundation, either version 3 of the+-- License, or (at your option) any later version.+--+-- This program is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with this program. If not, see <http://www.gnu.org/licenses/>.++module HOpenPGP.Tools.Common.Armor+ ( doDeArmor+ ) where++import qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA+import Codec.Encryption.OpenPGP.ASCIIArmor.Types (Armor(..))+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import Data.Conduit ((.|), runConduitRes)+import qualified Data.Conduit.Binary as CB+import qualified Data.Conduit.List as CL+import System.IO (hPutStrLn, stderr, stdin)++doDeArmor :: IO ()+doDeArmor = do+ a <- runConduitRes $ CB.sourceHandle stdin .| CL.consume+ case AA.decode (B.concat a) of+ Left e -> hPutStrLn stderr $ "Failure to decode ASCII Armor:" ++ e+ Right msgs -> BL.putStr $ BL.concat [ bs | Armor _ _ bs <- msgs ]
+ HOpenPGP/Tools/Common/Common.hs view
@@ -0,0 +1,242 @@+-- Common.hs: hOpenPGP-tools common functions+-- Copyright © 2012-2026 Clint Adams+--+-- vim: softtabstop=4:shiftwidth=4:expandtab+--+-- This program is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as+-- published by the Free Software Foundation, either version 3 of the+-- License, or (at your option) any later version.+--+-- This program is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with this program. If not, see <http://www.gnu.org/licenses/>.++module HOpenPGP.Tools.Common.Common+ ( banner+ , versioner+ , warranty+ , prependAuto+ , keyMatchesFingerprint+ , keyMatchesEightOctetKeyId+ , keyMatchesExactUIDString+ , keyMatchesUIDSubString+ , keyMatchesPKPred+ -- hmm+ , pkpGetPKVersion+ , pkpGetPKAlgo+ , pkpGetKeysize+ , pkpGetTimestamp+ , pkpGetFingerprint+ , pkpGetEOKI+ , tkUsingPKP+ , pUsingPKP+ , pUsingSP+ , tkGetUIDs+ , tkGetSubs+ , anyOrAll+ , anyReader+ , oGetTag+ , oGetLength+ , spGetSigVersion+ , spGetSigType+ , spGetPKAlgo+ , spGetHashAlgo+ , spGetSCT+ , maybeR+ , renderKeyID+ , renderFingerprint+ ) where++import Data.Version (showVersion)+import Paths_hopenpgp_tools (version)++import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)+import Codec.Encryption.OpenPGP.SignatureQualities (sigCT)+import Codec.Encryption.OpenPGP.Types+import Control.Lens ((^..))+import Data.Binary (put)+import Data.Binary.Put (runPut)+import qualified Data.ByteString.Lazy as BL+import Data.Data.Lens (biplate)+import Data.Text (Text)+import qualified Data.Text as T+import Options.Applicative.Builder (auto, help, hidden, infoOption, long, short)+import Options.Applicative.Types (Parser, ReadM(..))+import Prettyprinter (Doc, (<+>), hardline, pretty, defaultLayoutOptions, layoutPretty)+import qualified Prettyprinter.Render.Text as PPA+import Codec.Encryption.OpenPGP.KeyInfo (pubkeySize)+import Control.Error.Util (hush)+import Control.Monad.Trans.Reader+ ( Reader+ , ReaderT+ , ask+ , local+ , reader+ , runReader+ , withReader+ )+-- hmm --+import Data.Maybe (fromMaybe, mapMaybe)++banner :: String -> Doc ann+{-# INLINE banner #-}+banner name =+ pretty name <+>+ pretty "(hopenpgp-tools)" <+>+ pretty (showVersion version) <>+ hardline <> pretty "Copyright (C) 2012-2026 Clint Adams"++warranty :: String -> Doc ann+{-# INLINE warranty #-}+warranty name =+ pretty name <+>+ pretty "comes with ABSOLUTELY NO WARRANTY." <+>+ pretty "This is free software, and you are welcome to redistribute it" <+>+ pretty "under certain conditions."++versioner :: String -> Parser (a -> a)+{-# INLINE versioner #-}+versioner name =+ infoOption (name ++ " (hopenpgp-tools) " ++ showVersion version) $+ long "version" <> short 'V' <> help "Show version information" <> hidden++prependAuto :: Read a => String -> ReadM a+prependAuto s = ReadM (local (s ++) (unReadM auto))++keyMatchesFingerprint :: Bool -> TKUnknown -> Fingerprint -> Bool+keyMatchesFingerprint = keyMatchesPKPred fingerprint++keyMatchesEightOctetKeyId :: Bool -> TKUnknown -> Either String EightOctetKeyId -> Bool -- FIXME: refactor this somehow+keyMatchesEightOctetKeyId = keyMatchesPKPred eightOctetKeyID++keyMatchesExactUIDString :: Text -> TKUnknown -> Bool+keyMatchesExactUIDString uidstr = elem uidstr . map fst . _tkuUIDs++keyMatchesUIDSubString :: Text -> TKUnknown -> Bool+keyMatchesUIDSubString uidstr =+ any (T.toLower uidstr `T.isInfixOf`) . map (T.toLower . fst) . _tkuUIDs++keyMatchesPKPred :: Eq a => (SomePKPayload -> a) -> Bool -> TKUnknown -> a -> Bool+keyMatchesPKPred p False = (==) . p . fst . _tkuKey+keyMatchesPKPred p True = \tk v -> elem v (map p (tk ^.. biplate))++-- The following should probably be moved elsewhere+tkUsingPKP :: Reader SomePKPayload a -> Reader TKUnknown a+tkUsingPKP = withReader (fst . _tkuKey)++pkpGetPKVersion :: SomePKPayload -> Integer+pkpGetPKVersion t =+ if _keyVersion t == DeprecatedV3+ then 3+ else 4++pkpGetPKAlgo :: SomePKPayload -> Integer+pkpGetPKAlgo = fromIntegral . fromFVal . _pkalgo++pkpGetKeysize :: SomePKPayload -> Integer+pkpGetKeysize = fromIntegral . fromMaybe 0 . hush . pubkeySize . _pubkey++pkpGetTimestamp :: SomePKPayload -> Integer+pkpGetTimestamp = fromIntegral . _timestamp++pkpGetFingerprint :: SomePKPayload -> Fingerprint+pkpGetFingerprint = fingerprint++pkpGetEOKI :: SomePKPayload -> String+pkpGetEOKI = either (const "UNKNOWN") show . eightOctetKeyID++tkGetUIDs :: TKUnknown -> [Text]+tkGetUIDs = map fst . _tkuUIDs++tkGetSubs :: TKUnknown -> [SomePKPayload]+tkGetSubs = mapMaybe (grabPKP . fst) . _tkuSubs+ where+ grabPKP (PublicSubkeyPkt p) = Just p+ grabPKP (SecretSubkeyPkt p _) = Just p+ grabPKP _ = Nothing++anyOrAll ::+ (Monad m, Monad m1)+ => ((a1 -> c) -> a -> ReaderT a m b)+ -> (m1 a1 -> c)+ -> ReaderT a m b+anyOrAll aa op = ask >>= aa (op . return)++anyReader :: Reader a Bool -> Reader [a] Bool+anyReader p = any (runReader p) `fmap` ask++oGetTag :: Pkt -> Integer+oGetTag = fromIntegral . pktTag++oGetLength :: Pkt -> Integer+oGetLength = fromIntegral . BL.length . runPut . put -- FIXME: this should be a length that makes sense++spGetSigVersion :: Pkt -> Maybe Integer+spGetSigVersion (SignaturePkt s) = Just (sigVersion s)+ where+ sigVersion SigV3 {} = 3+ sigVersion SigV4 {} = 4+ sigVersion SigV6 {} = 6+ sigVersion (SigVOther v _) = fromIntegral v+spGetSigVersion _ = Nothing++spGetSigType :: Pkt -> Maybe Integer+spGetSigType (SignaturePkt s) = fmap (fromIntegral . fromFVal) (sigType s)+ -- FIXME: deduplicate this and hOpenPGP .Internal+ where+ sigType :: SignaturePayload -> Maybe SigType+ sigType (SigV3 st _ _ _ _ _ _) = Just st+ sigType (SigV4 st _ _ _ _ _ _) = Just st+ sigType _ = Nothing -- this includes v2 sigs, which don't seem to be specified in the RFCs but exist in the wild+spGetSigType _ = Nothing++spGetPKAlgo :: Pkt -> Maybe Integer+spGetPKAlgo (SignaturePkt s) = fmap (fromIntegral . fromFVal) (sigPKA s)+ where+ sigPKA (SigV3 _ _ _ pka _ _ _) = Just pka+ sigPKA (SigV4 _ pka _ _ _ _ _) = Just pka+ sigPKA _ = Nothing -- this includes v2 sigs, which don't seem to be specified in the RFCs but exist in the wild+spGetPKAlgo _ = Nothing++spGetHashAlgo :: Pkt -> Maybe Integer+spGetHashAlgo (SignaturePkt s) = fmap (fromIntegral . fromFVal) (sigHA s)+ where+ sigHA (SigV3 _ _ _ _ ha _ _) = Just ha+ sigHA (SigV4 _ _ ha _ _ _ _) = Just ha+ sigHA _ = Nothing -- this includes v2 sigs, which don't seem to be specified in the RFCs but exist in the wild+spGetHashAlgo _ = Nothing++spGetSCT :: Pkt -> Maybe Integer+spGetSCT (SignaturePkt s) = fmap fromIntegral (sigCT s)+spGetSCT _ = Nothing++pUsingPKP :: Reader (Maybe SomePKPayload) a -> Reader Pkt a+pUsingPKP = withReader grabPayload+ where+ grabPayload (SecretKeyPkt p _) = Just p+ grabPayload (PublicKeyPkt p) = Just p+ grabPayload (SecretSubkeyPkt p _) = Just p+ grabPayload (PublicSubkeyPkt p) = Just p+ grabPayload _ = Nothing++pUsingSP :: Reader (Maybe SignaturePayload) a -> Reader Pkt a+pUsingSP = withReader grabPayload+ where+ grabPayload (SignaturePkt s) = Just s+ grabPayload _ = Nothing++maybeR :: a -> Reader r a -> Reader (Maybe r) a+maybeR x r = reader (maybe x (runReader r))++renderKeyID :: EightOctetKeyId -> String+renderKeyID =+ T.unpack . PPA.renderStrict . layoutPretty defaultLayoutOptions . pretty++renderFingerprint :: Fingerprint -> String+renderFingerprint =+ T.unpack . PPA.renderStrict . layoutPretty defaultLayoutOptions . pretty
+ HOpenPGP/Tools/Common/HKP.hs view
@@ -0,0 +1,126 @@+-- HKP.hs: hOpenPGP key tool+-- Copyright © 2016-2026 Clint Adams+--+-- vim: softtabstop=4:shiftwidth=4:expandtab+--+-- This program is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as+-- published by the Free Software Foundation, either version 3 of the+-- License, or (at your option) any later version.+--+-- This program is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with this program. If not, see <http://www.gnu.org/licenses/>.++{-# LANGUAGE OverloadedStrings #-}++module HOpenPGP.Tools.Common.HKP+ ( fetchKeys+ , FetchValidationMethod(..)+ , rearmorKeys+ ) where++import qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA+import Codec.Encryption.OpenPGP.ASCIIArmor.Types+ ( Armor(Armor)+ , ArmorType(ArmorPublicKeyBlock)+ )+import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)+import Codec.Encryption.OpenPGP.Types+ ( Block(..)+ , TKUnknown(..)+ , Fingerprint+ )+import Control.Applicative (liftA2)+import Control.Arrow ((&&&))+import Control.Lens ((^..))+import Control.Monad.IO.Class (liftIO)+import Control.Monad.Trans.Except (ExceptT(..), throwE)+import Data.Binary (get, put)+import Data.Binary.Put (runPut)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC8+import qualified Data.ByteString.Lazy as BL+import Data.Conduit ((.|), runConduitRes)+import qualified Data.Conduit.Binary as CB+import qualified Data.Conduit.List as CL+import Data.Conduit.OpenPGP.Keyring (conduitToTKsDropping)+import Data.Conduit.Serialization.Binary (conduitGet)+import Data.Data.Lens (biplate)+import Data.Either (rights)+import Data.Monoid ((<>), mempty)+import Data.Time.Clock.POSIX (getPOSIXTime)+import HOpenPGP.Tools.Common.TKUtils (processTK)+import Network.HTTP.Client+ ( Response(..)+ , httpLbs+ , newManager+ , parseUrlThrow+ , setQueryString+ )+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Network.HTTP.Types.Status (ok200)+import Prettyprinter (pretty)++data FetchValidationMethod+ = MatchPrimaryKeyFingerprint+ | MatchPrimaryOrAnySubkeyFingerprint+ | AnySelfSigned+ deriving (Bounded, Enum, Eq, Read, Show)++fetchKeys ::+ String+ -> FetchValidationMethod+ -> Fingerprint+ -> ExceptT String IO [TKUnknown]+fetchKeys ks fvm q = do+ manager <- liftIO $ newManager tlsManagerSettings+ request <- liftIO $ parseUrlThrow (ks <> basereq)+ let newreq = setQueryString (newqs q) request+ response <- liftIO $ httpLbs newreq manager+ processedKeys <-+ if responseStatus response == ok200+ then validateKeys (responseBody response)+ else throwE ("HTTP status: " ++ show (responseStatus response))+ return $ map fst $ filter (fvp fvm . fst . _tkuKey . snd) processedKeys+ where+ fvp MatchPrimaryKeyFingerprint k = fingerprint k == q+ fvp MatchPrimaryOrAnySubkeyFingerprint k =+ any (\k -> fingerprint k == q) (k ^.. biplate)+ fvp AnySelfSigned k = True+ basereq = "/pks/lookup"+ newqs q =+ [ ("op", Just "get")+ , ("options", Just "mr")+ , ("exact", Just "on")+ , ("search", Just (BC8.pack ("0x" <> show (pretty q)))) -- FIXME: butter+ ]++validateKeys :: BL.ByteString -> ExceptT String IO [(TKUnknown, TKUnknown)] -- FIXME: conduit fail+validateKeys larmors = do+ bytestrings <-+ ExceptT $ return $ fmap (mconcat . map armorToBS) (AA.decodeLazy larmors)+ keys <-+ liftIO . runConduitRes $+ CB.sourceLbs bytestrings .| conduitGet get .| conduitToTKsDropping .|+ CL.consume+ cpt <- liftIO getPOSIXTime+ return . rights $+ map (uncurry (liftA2 (,)) . (pure &&& processTK (Just cpt))) keys+ where+ armorToBS (Armor ArmorPublicKeyBlock _ bs) = bs+ armorToBS _ = mempty++rearmorKeys :: [TKUnknown] -> B.ByteString+rearmorKeys keys =+ if null keys+ then mempty+ else AA.encode .+ return .+ Armor ArmorPublicKeyBlock [("Comment", "filtered by hokey")] .+ runPut . put . Block $+ keys
+ HOpenPGP/Tools/Common/Lexer.x view
@@ -0,0 +1,175 @@+{+{-# OPTIONS -w #-}+module HOpenPGP.Tools.Common.Lexer+( alexEOF+, alexSetInput+, alexGetInput+, alexError+, alexScan+, ignorePendingBytes+, alexGetStartCode+, runAlex+, Alex(..)+, Token(..)+, AlexReturn(..)+, AlexPosn(..)+) where++import Prelude hiding (lex)+import Numeric (readHex)+import Codec.Encryption.OpenPGP.Types (Fingerprint(..), EightOctetKeyId(..))++}++%wrapper "monad"++$digit = 0-9+$hexdigit = [0-9A-Fa-f]++tokens :-+ $white+ ;+ a { lex' TokenA }+ and { lex' TokenAnd }+ any { lex' TokenAny }+ every { lex' TokenEvery }+ not { lex' TokenNot }+ now { lex' TokenNow }+ one { lex' TokenOne }+ or { lex' TokenOr }+ subkey { lex' TokenSubkey }+ tag { lex' TokenTag }+ of { lex' TokenOf }+ \=\= { lex' TokenEq }+ \= { lex' TokenEq }+ equals { lex' TokenEq }+ \< { lex' TokenLt }+ \> { lex' TokenGt }+ \( { lex' TokenLParen }+ \) { lex' TokenRParen }+ contains { lex' TokenContains }+ pkversion { lex' TokenPKVersion }+ sigversion { lex' TokenSigVersion }+ [Ss]ig[Tt]ype { lex' TokenSigType }+ [Pp][Kk][Aa]lgo { lex' TokenPKAlgo }+ [Ss]ig[Pp][Kk][Aa]lgo { lex' TokenSigPKAlgo }+ [Hh]ash[Aa]lgo { lex' TokenHashAlgo }+ [Rr][Ss][Aa] { lex' TokenRSA }+ [Dd][Ss][Aa] { lex' TokenDSA }+ [Ee]l[Gg]amal { lex' TokenElgamal }+ [Ee][Cc][Dd][Ss][Aa] { lex' TokenECDSA }+ [Ee][Cc][Dd][Hh] { lex' TokenECDH }+ [Dd][Hh] { lex' TokenDH }+ [Bb]inary { lex' TokenBinary }+ [Cc]anonical[Tt]ext { lex' TokenCanonicalText }+ [Ss]tandalone { lex' TokenStandalone }+ [Gg]eneric[Cc]ert { lex' TokenGenericCert }+ [Pp]ersona[Cc]ert { lex' TokenPersonaCert }+ [Cc]asual[Cc]ert { lex' TokenCasualCert }+ [Pp]ositive[Cc]ert { lex' TokenPositiveCert }+ [Ss]ubkey[Bb]inding[Ss]ig { lex' TokenSubkeyBindingSig }+ [Pp]rimary[Kk]ey[Bb]inding[Ss]ig { lex' TokenPrimaryKeyBindingSig }+ [Ss]ignature[Dd]irectly[Oo]n[Aa][Kk]ey { lex' TokenSignatureDirectlyOnAKey }+ [Kk]ey[Rr]evocation[Ss]ig { lex' TokenKeyRevocationSig }+ [Ss]ubkey[Rr]evocation[Ss]ig { lex' TokenSubkeyRevocationSig }+ [Cc]ert[Rr]evocation[Ss]ig { lex' TokenCertRevocationSig }+ [Tt]imestamp[Ss]ig { lex' TokenTimestampSig }+ [Mm][Dd]5 { lex' TokenMD5 }+ [Ss][Hh][Aa]1 { lex' TokenSHA1 }+ [Rr][Ii][Pp][Ee][Mm][Dd]160 { lex' TokenRIPEMD160 }+ [Ss][Hh][Aa]256 { lex' TokenSHA256 }+ [Ss][Hh][Aa]384 { lex' TokenSHA384 }+ [Ss][Hh][Aa]512 { lex' TokenSHA512 }+ [Ss][Hh][Aa]224 { lex' TokenSHA224 }+ [Uu][Ii][Dd]s { lex' TokenUids }+ keysize { lex' TokenKeysize }+ length { lex' TokenLength }+ timestamp { lex' TokenTimestamp }+ fingerprint { lex' TokenFingerprint }+ keyid { lex' TokenKeyID }+ [Ss]ig[Cc]reation[Tt]ime { lex' TokenSigCreationTime }+ $hexdigit{9}$hexdigit{9}$hexdigit{9}$hexdigit{9}$hexdigit{4} { lex (TokenFpr . read) }+ 0x$hexdigit{9}$hexdigit{9}$hexdigit{9}$hexdigit{9}$hexdigit{4} { lex (TokenFpr . read . drop 2) }+ $hexdigit{8}$hexdigit{8} { lex (TokenLongID . Right . read) }+ 0x$hexdigit{8}$hexdigit{8} { lex (TokenLongID . Right . read . drop 2) }+ $digit+ { lex (TokenInt . fromIntegral . read) }+ $hexdigit+ { lex (TokenInt . fromIntegral . fst . head . readHex) }+ 0x$hexdigit+ { lex (TokenInt . fromIntegral . fst . head . readHex . drop 2) }+ \".*\" { lex (TokenStr . ((zipWith const . drop 1) <*> (drop 2))) }++{+data Token+ = TokenTag+ | TokenAfter+ | TokenAnd+ | TokenAny+ | TokenBefore+ | TokenNot+ | TokenNow+ | TokenOr+ | TokenInt Integer+ | TokenEq+ | TokenLt+ | TokenGt+ | TokenLParen+ | TokenRParen+ | TokenEOF+ | TokenPKVersion+ | TokenSigVersion+ | TokenSigType+ | TokenPKAlgo+ | TokenSigPKAlgo+ | TokenHashAlgo+ | TokenRSA+ | TokenDSA+ | TokenElgamal+ | TokenECDSA+ | TokenECDH+ | TokenDH+ | TokenBinary+ | TokenCanonicalText+ | TokenStandalone+ | TokenGenericCert+ | TokenPersonaCert+ | TokenCasualCert+ | TokenPositiveCert+ | TokenSubkeyBindingSig+ | TokenPrimaryKeyBindingSig+ | TokenSignatureDirectlyOnAKey+ | TokenKeyRevocationSig+ | TokenSubkeyRevocationSig+ | TokenCertRevocationSig+ | TokenTimestampSig+ | TokenMD5+ | TokenSHA1+ | TokenRIPEMD160+ | TokenSHA256+ | TokenSHA384+ | TokenSHA512+ | TokenSHA224+ | TokenKeysize+ | TokenTimestamp+ | TokenFingerprint+ | TokenKeyID+ | TokenFpr Fingerprint+ | TokenLongID (Either String EightOctetKeyId)+ | TokenLength+ | TokenEvery+ | TokenOne+ | TokenOf+ | TokenContains+ | TokenUids+ | TokenStr String+ | TokenA+ | TokenSubkey+ | TokenSigCreationTime+ deriving (Eq,Show)++alexEOF = return TokenEOF++lex :: (String -> a) -> AlexAction a+lex f = \(_,_,_,s) i -> return (f (take i s))++lex' :: a -> AlexAction a+lex' = lex . const++}
+ HOpenPGP/Tools/Common/Parser.y view
@@ -0,0 +1,229 @@+{+{-# OPTIONS -w #-}+module HOpenPGP.Tools.Common.Parser( parseTKExp, parsePExp ) where+import Codec.Encryption.OpenPGP.Types+-- import Data.Conduit.OpenPGP.Filter (Expr(..), UPredicate(..), UOp(..), OVar(..), OValue(..), SPVar(..), SPValue(..), PKPVar(..), PKPValue(..))+import HOpenPGP.Tools.Common.Common (pkpGetPKVersion, pkpGetPKAlgo, pkpGetKeysize, pkpGetTimestamp, pkpGetFingerprint, pkpGetEOKI, tkUsingPKP, tkGetUIDs, tkGetSubs, anyOrAll, anyReader, oGetTag, oGetLength, spGetSigVersion, spGetSigType, spGetPKAlgo, spGetHashAlgo, spGetSCT, pUsingPKP, pUsingSP, maybeR)++import HOpenPGP.Tools.Common.Lexer++import Control.Applicative (liftA2)+import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)+import Codec.Encryption.OpenPGP.KeyInfo (pubkeySize)+import Control.Error.Util (hush)+import Control.Monad.Loops (allM, anyM)+import Control.Monad.Trans.Reader (ask, reader, Reader, withReader)+import Data.List (isInfixOf)+import qualified Data.Text as T+import Prettyprinter (pretty)++}++%name parseTK Exp+%name parseP CFExp+%tokentype { Token }+%monad { Alex }+%lexer { lexwrap } { TokenEOF }+%error { happyError }+%token+ a { TokenA }+ and { TokenAnd }+ any { TokenAny }+ contains { TokenContains }+ every { TokenEvery }+ not { TokenNot }+ now { TokenNow }+ of { TokenOf }+ one { TokenOne }+ or { TokenOr }+ subkey { TokenSubkey }+ tag { TokenTag }+ int { TokenInt $$ }+ '=' { TokenEq }+ '<' { TokenLt }+ '>' { TokenGt }+ '(' { TokenLParen }+ ')' { TokenRParen }+ pkversion { TokenPKVersion }+ sigversion { TokenSigVersion }+ sigtype { TokenSigType }+ pkalgo { TokenPKAlgo }+ sigpkalgo { TokenSigPKAlgo }+ hashalgo { TokenHashAlgo }+ rsa { TokenRSA }+ dsa { TokenDSA }+ elgamal { TokenElgamal }+ ecdsa { TokenECDSA }+ ecdh { TokenECDH }+ dh { TokenDH }+ binary { TokenBinary }+ canonicaltext { TokenCanonicalText }+ standalone { TokenStandalone }+ genericcert { TokenGenericCert }+ personacert { TokenPersonaCert }+ casualcert { TokenCasualCert }+ positivecert { TokenPositiveCert }+ subkeybindingsig { TokenSubkeyBindingSig }+ primarykeybindingsig { TokenPrimaryKeyBindingSig }+ signaturedirectlyonakey { TokenSignatureDirectlyOnAKey }+ keyrevocationsig { TokenKeyRevocationSig }+ subkeyrevocationsig { TokenSubkeyRevocationSig }+ certrevocationsig { TokenCertRevocationSig }+ timestampsig { TokenTimestampSig }+ md5 { TokenMD5 }+ sha1 { TokenSHA1 }+ ripemd160 { TokenRIPEMD160 }+ sha256 { TokenSHA256 }+ sha384 { TokenSHA384 }+ sha512 { TokenSHA512 }+ sha224 { TokenSHA224 }+ keysize { TokenKeysize }+ timestamp { TokenTimestamp }+ fingerprint { TokenFingerprint }+ keyid { TokenKeyID }+ sigcreationtime { TokenSigCreationTime }+ fpr { TokenFpr $$ }+ longid { TokenLongID $$ }+ length { TokenLength }+ str { TokenStr $$ }+ uids { TokenUids }++%%++Exp : any { return True }+ | not Exp { fmap not $2 }+ | Exp and Exp { liftA2 (&&) $1 $3 }+ | Exp or Exp { liftA2 (||) $1 $3 }+ | PExp { tkUsingPKP $1 }+ | TExp { $1 }++PExp : pkversion PIOp int { $2 (reader pkpGetPKVersion) (return $3) }+ | pkalgo PIOp Ppkalgos { $2 (reader pkpGetPKAlgo) (return $3) }+ | keysize PIOp int { $2 (reader pkpGetKeysize) (return $3) }+ | timestamp PIOp int { $2 (reader pkpGetTimestamp) (return $3) }+ | fingerprint PSOp Pfingerprint { $2 (reader (show . pretty . pkpGetFingerprint)) (return $3) }+ | keyid PSOp Plongid { $2 (reader pkpGetEOKI) (return $3) }++TExp : every one of uids AATOp str { withReader tkGetUIDs (anyOrAll allM ($5 (return (T.pack $6)))) }+ | any one of uids AATOp str { withReader tkGetUIDs (anyOrAll anyM ($5 (return (T.pack $6)))) }+ | any of uids AATOp str { withReader tkGetUIDs (anyOrAll anyM ($4 (return (T.pack $5)))) }+ | a subkey PExp { withReader tkGetSubs (anyReader $3) }++PIOp : '=' { liftA2 (==) }+ | '<' { liftA2 (<) }+ | '>' { liftA2 (>) }++PSOp : '=' { liftA2 (==) }+ | contains { liftA2 (flip isInfixOf) }++AATOp : '=' { liftA2 (==) }+ | contains { liftA2 T.isInfixOf }++Ppkalgos : rsa { fromIntegral (fromFVal RSA) }+ | dsa { fromIntegral (fromFVal DSA) }+ | elgamal { fromIntegral (fromFVal ElgamalEncryptOnly) }+ | ecdsa { fromIntegral (fromFVal ECDSA) }+ | ecdh { fromIntegral (fromFVal ECDH) }+ | dh { fromIntegral (fromFVal DH) }+ | int { fromIntegral $1 }++Pfingerprint : fpr { (show . pretty) $1 }++Plongid : longid { either (const "BROKEN") show $1 }++CFExp : any { return True }+ | not CFExp { fmap not $2 }+ | CFExp and CFExp { liftA2 (&&) $1 $3 }+ | CFExp or CFExp { liftA2 (||) $1 $3 }+ | OExp { $1 }+ | SPExp { $1 }+ | PExp { pUsingPKP (maybeR True $1) }++OExp : tag OIOp int { $2 (reader oGetTag) (return $3) }+ | length OIOp int { $2 (reader oGetLength) (return $3) }++OIOp : '=' { liftA2 (==) }+ | '<' { liftA2 (<) }+ | '>' { liftA2 (>) }++SPExp : sigversion SIOp int { $2 (reader spGetSigVersion) (return (Just $3)) }+ | sigtype SIOp Ssigtypes { $2 (reader spGetSigType) (return (Just $3)) }+ | sigpkalgo SIOp Spkalgos { $2 (reader spGetPKAlgo) (return (Just $3)) }+ | hashalgo SIOp Shashalgos { $2 (reader spGetHashAlgo) (return (Just $3)) }+ | sigcreationtime SIOp Stimespec { $2 (reader spGetSCT) (return (Just $3)) }++SIOp : '=' { liftA2 (==) }+ | '<' { liftA2 (<) }+ | '>' { liftA2 (>) }++Ssigtypes : binary { fromIntegral (fromFVal BinarySig) }+ | canonicaltext { fromIntegral (fromFVal CanonicalTextSig) }+ | standalone { fromIntegral (fromFVal StandaloneSig) }+ | genericcert { fromIntegral (fromFVal GenericCert) }+ | personacert { fromIntegral (fromFVal PersonaCert) }+ | casualcert { fromIntegral (fromFVal CasualCert) }+ | positivecert { fromIntegral (fromFVal PositiveCert) }+ | subkeybindingsig { fromIntegral (fromFVal SubkeyBindingSig) }+ | primarykeybindingsig { fromIntegral (fromFVal PrimaryKeyBindingSig) }+ | signaturedirectlyonakey { fromIntegral (fromFVal SignatureDirectlyOnAKey) }+ | keyrevocationsig { fromIntegral (fromFVal KeyRevocationSig) }+ | subkeyrevocationsig { fromIntegral (fromFVal SubkeyRevocationSig) }+ | certrevocationsig { fromIntegral (fromFVal CertRevocationSig) }+ | timestampsig { fromIntegral (fromFVal TimestampSig) }+ | int { fromIntegral $1 }++Spkalgos : rsa { fromIntegral (fromFVal RSA) }+ | dsa { fromIntegral (fromFVal DSA) }+ | elgamal { fromIntegral (fromFVal ElgamalEncryptOnly) }+ | ecdsa { fromIntegral (fromFVal ECDSA) }+ | ecdh { fromIntegral (fromFVal ECDH) }+ | dh { fromIntegral (fromFVal DH) }+ | int { fromIntegral $1 }++Shashalgos : md5 { fromIntegral (fromFVal DeprecatedMD5) }+ | sha1 { fromIntegral (fromFVal SHA1) }+ | ripemd160 { fromIntegral (fromFVal RIPEMD160) }+ | sha256 { fromIntegral (fromFVal SHA256) }+ | sha384 { fromIntegral (fromFVal SHA384) }+ | sha512 { fromIntegral (fromFVal SHA512) }+ | sha224 { fromIntegral (fromFVal SHA224) }+ | int { fromIntegral $1 }++Stimespec : now { 0 }+ | int { fromIntegral $1 }++{+lexwrap :: (Token -> Alex a) -> Alex a+lexwrap cont = do+ t <- alexMonadScan'+ cont t++alexMonadScan' = do+ inp <- alexGetInput+ sc <- alexGetStartCode+ case alexScan inp sc of+ AlexEOF -> alexEOF+ AlexError (pos, _, _, _) -> alexError (show pos)+ AlexSkip inp' len -> do+ alexSetInput inp'+ alexMonadScan'+ AlexToken inp' len action -> do+ alexSetInput inp'+ action (ignorePendingBytes inp) len++getPosn :: Alex (Int,Int)+getPosn = do+ (AlexPn _ l c,_,_,_) <- alexGetInput+ return (l,c)++happyError :: Token -> Alex a+happyError t = do+ (l,c) <- getPosn+ error (show l ++ ":" ++ show c ++ ": Parse error on Token: " ++ show t ++ "\n")++parseTKExp :: String -> Either String (Reader TKUnknown Bool)+parseTKExp s = runAlex s parseTK++parsePExp :: String -> Either String (Reader Pkt Bool)+parsePExp s = runAlex s parseP+}
+ HOpenPGP/Tools/Common/TKUtils.hs view
@@ -0,0 +1,94 @@+-- TKUtils.hs: hOpenPGP-tools TK-related common functions+-- Copyright © 2013-2026 Clint Adams+--+-- vim: softtabstop=4:shiftwidth=4:expandtab+--+-- This program is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as+-- published by the Free Software Foundation, either version 3 of the+-- License, or (at your option) any later version.+--+-- This program is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with this program. If not, see <http://www.gnu.org/licenses/>.++module HOpenPGP.Tools.Common.TKUtils+ ( processTK+ ) where++import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)+import Codec.Encryption.OpenPGP.Signatures+ ( VerificationError+ , verifyAgainstKeys+ , verifySigWith+ , verifyUnknownTKWith+ )+import Codec.Encryption.OpenPGP.Types+import Control.Arrow (second)+import Control.Error.Util (hush)+import Control.Lens ((^.), _1)+import Data.Bifunctor (first)+import Data.List (sortOn)+import Data.Maybe (listToMaybe, mapMaybe)+import Data.Ord (Down(..))+import Data.Time.Clock.POSIX (POSIXTime, posixSecondsToUTCTime)++-- should this fail or should verifyUnknownTKWith fail if there are no self-sigs?+processTK :: Maybe POSIXTime -> TKUnknown -> Either String TKUnknown+processTK mpt key =+ first show $ verifyUnknownTKWith+ (verifySigWith (verifyAgainstKeys [key]))+ (fmap posixSecondsToUTCTime mpt) .+ stripOlderSigs .+ stripOtherSigs $+ key+ where+ stripOtherSigs tk =+ tk+ { _tkuUIDs = map (second alleged) (_tkuUIDs tk)+ , _tkuUAts = map (second alleged) (_tkuUAts tk)+ }+ stripOlderSigs tk =+ tk+ { _tkuUIDs = map (second newest) (_tkuUIDs tk)+ , _tkuUAts = map (second newest) (_tkuUAts tk)+ }+ newest = take 1 . sortOn (Down . take 1 . sigcts) -- FIXME: this is terrible+ sigcts (SigV4 _ _ _ xs _ _ _) = mapMaybe sigCreationTimeFromSubpacket xs+ sigcts (SigV6 _ _ _ _ xs _ _ _) = mapMaybe sigCreationTimeFromSubpacket xs+ sigcts _ = []+ pkp = key ^. tkuKey . _1+ alleged = filter (\x -> assI x || assIFP x)+ sigCreationTimeFromSubpacket (SigSubPacket _ (SigCreationTime x)) = Just x+ sigCreationTimeFromSubpacket _ = Nothing+ sigissuer (SigVOther 2 _) = Nothing+ sigissuer SigV3 {} = Nothing+ sigissuer (SigV4 _ _ _ ys xs _ _) =+ listToMaybe . mapMaybe (getIssuer . _sspPayload) $ (ys ++ xs) -- FIXME: what should this be if there are multiple matches?+ sigissuer (SigV6 _ _ _ _ ys xs _ _) =+ listToMaybe . mapMaybe (getIssuer . _sspPayload) $ (ys ++ xs) -- FIXME: what should this be if there are multiple matches?+ sigissuer (SigVOther _ _) = Nothing+ sigissuerfp (SigV4 _ _ _ ys xs _ _) =+ listToMaybe . mapMaybe (getIssuerFP . _sspPayload) $ (ys ++ xs) -- FIXME: what should this be if there are multiple matches?+ sigissuerfp (SigV6 _ _ _ _ ys xs _ _) =+ listToMaybe . mapMaybe (getIssuerFP . _sspPayload) $ (ys ++ xs) -- FIXME: what should this be if there are multiple matches?+ sigissuerfp _ = Nothing+ eoki+ | _keyVersion pkp == V4 = hush . eightOctetKeyID $ pkp+ | _keyVersion pkp == DeprecatedV3 &&+ elem (_pkalgo pkp) [RSA, DeprecatedRSASignOnly] =+ hush . eightOctetKeyID $ pkp+ | otherwise = Nothing+ fp+ | _keyVersion pkp == V4 = Just . fingerprint $ pkp+ | otherwise = Nothing+ getIssuer (Issuer i) = Just i+ getIssuer _ = Nothing+ getIssuerFP (IssuerFingerprint IssuerFingerprintV4 i) = Just i+ getIssuerFP _ = Nothing+ assI x = ((==) <$> sigissuer x <*> eoki) == Just True+ assIFP x = ((==) <$> sigissuerfp x <*> fp) == Just True
+ HOpenPGP/Tools/Common/WKD.hs view
@@ -0,0 +1,183 @@+-- WKD.hs: hOpenPGP key tool+-- Copyright © 2026 Clint Adams+--+-- vim: softtabstop=4:shiftwidth=4:expandtab+--+-- This program is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as+-- published by the Free Software Foundation, either version 3 of the+-- License, or (at your option) any later version.+--+-- This program is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with this program. If not, see <http://www.gnu.org/licenses/>.++{-# LANGUAGE OverloadedStrings #-}++module HOpenPGP.Tools.Common.WKD+ ( fetchKeys+ , parseMailbox+ ) where++import qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA+import Codec.Encryption.OpenPGP.ASCIIArmor.Types+ ( Armor(Armor)+ , ArmorType(ArmorPublicKeyBlock)+ )+import Codec.Encryption.OpenPGP.Types (TKUnknown(..))+import Control.Arrow ((&&&))+import Control.Monad.IO.Class (liftIO)+import Control.Monad.Trans.Except (ExceptT(..), throwE)+import qualified Crypto.Hash as CH+import qualified Crypto.Hash.Algorithms as CHA+import Data.Binary (get)+import qualified Data.ByteArray as BA+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC8+import qualified Data.ByteString.Lazy as BL+import Data.Bits ((.&.), (.|.), shiftL, shiftR)+import Data.Conduit ((.|), runConduitRes)+import qualified Data.Conduit.Binary as CB+import qualified Data.Conduit.List as CL+import Data.Conduit.OpenPGP.Keyring (conduitToTKsDropping)+import Data.Conduit.Serialization.Binary (conduitGet)+import Data.Either (rights)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import Data.Time.Clock.POSIX (getPOSIXTime)+import Data.Word (Word8)+import HOpenPGP.Tools.Common.HKP (FetchValidationMethod(..))+import HOpenPGP.Tools.Common.TKUtils (processTK)+import Network.HTTP.Client+ ( Manager+ , Response(..)+ , httpLbs+ , newManager+ , parseUrlThrow+ , setQueryString+ )+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Network.HTTP.Types.Status (ok200)++fetchKeys ::+ FetchValidationMethod+ -> Text+ -> ExceptT String IO [TKUnknown]+fetchKeys fvm mailbox = do+ parsedMailbox <- ExceptT . return $ parseMailbox mailbox+ manager <- liftIO $ newManager tlsManagerSettings+ response <- fetchWKD manager parsedMailbox+ body <-+ if responseStatus response == ok200+ then return (responseBody response)+ else throwE ("HTTP status: " ++ show (responseStatus response))+ validateAndFilterKeys fvm parsedMailbox body++parseMailbox :: Text -> Either String (Text, Text)+parseMailbox rawMailbox =+ let mailbox = T.strip rawMailbox+ parts = T.splitOn "@" mailbox+ in case parts of+ [localPart, domain]+ | T.null localPart -> Left "mailbox local part cannot be empty"+ | T.null domain -> Left "mailbox domain cannot be empty"+ | T.any (== ' ') mailbox -> Left "mailbox cannot contain spaces"+ | otherwise -> Right (localPart, T.toLower domain)+ _ -> Left "mailbox must contain exactly one @"++fetchWKD ::+ Manager+ -> (Text, Text)+ -> ExceptT String IO (Response BL.ByteString)+fetchWKD manager (localPart, domain) = do+ let localPartLower = T.toLower localPart+ hu = BC8.unpack . zbase32 . sha1 . TE.encodeUtf8 $ localPartLower+ advancedUrl =+ "https://openpgpkey." <>+ T.unpack domain <>+ "/.well-known/openpgpkey/" <> T.unpack domain <> "/hu/" <> hu+ directUrl =+ "https://" <> T.unpack domain <> "/.well-known/openpgpkey/hu/" <> hu+ mailboxParam = TE.encodeUtf8 localPartLower+ withMailbox req = setQueryString [("l", Just mailboxParam)] req+ advancedRequest <- liftIO $ parseUrlThrow advancedUrl+ advancedResponse <- liftIO $ httpLbs (withMailbox advancedRequest) manager+ if responseStatus advancedResponse == ok200+ then return advancedResponse+ else do+ directRequest <- liftIO $ parseUrlThrow directUrl+ liftIO $ httpLbs (withMailbox directRequest) manager++validateAndFilterKeys ::+ FetchValidationMethod+ -> (Text, Text)+ -> BL.ByteString+ -> ExceptT String IO [TKUnknown]+validateAndFilterKeys fvm mailbox body = do+ keys <- decodeWkdResponse body+ cpt <- liftIO getPOSIXTime+ let processedKeys = rights $ map (uncurry (liftA2 (,)) . (pure &&& processTK (Just cpt))) keys+ mailboxFiltered = filter (mailboxMatchesKey mailbox . snd) processedKeys+ return $+ map fst $+ case fvm of+ AnySelfSigned -> processedKeys+ MatchPrimaryKeyFingerprint -> mailboxFiltered+ MatchPrimaryOrAnySubkeyFingerprint -> mailboxFiltered++decodeWkdResponse :: BL.ByteString -> ExceptT String IO [TKUnknown]+decodeWkdResponse body =+ if isArmored body+ then decodeArmored body+ else decodeBinary body++decodeBinary :: BL.ByteString -> ExceptT String IO [TKUnknown]+decodeBinary bytes =+ liftIO . runConduitRes $+ CB.sourceLbs bytes .| conduitGet get .| conduitToTKsDropping .| CL.consume++decodeArmored :: BL.ByteString -> ExceptT String IO [TKUnknown]+decodeArmored larmors = do+ bytestrings <- ExceptT . return $ fmap (mconcat . map armorToBS) (AA.decodeLazy larmors)+ liftIO . runConduitRes $+ CB.sourceLbs bytestrings .| conduitGet get .| conduitToTKsDropping .| CL.consume+ where+ armorToBS (Armor ArmorPublicKeyBlock _ bs) = bs+ armorToBS _ = mempty++isArmored :: BL.ByteString -> Bool+isArmored =+ BC8.isPrefixOf "-----BEGIN PGP PUBLIC KEY BLOCK-----" . BL.toStrict . BL.take 40++mailboxMatchesKey :: (Text, Text) -> TKUnknown -> Bool+mailboxMatchesKey (localPart, domain) tk =+ let mailbox = T.toLower (localPart <> "@" <> domain)+ bracketedMailbox = "<" <> mailbox <> ">"+ in any+ (\uid -> let lowered = T.toLower uid in lowered == mailbox || bracketedMailbox `T.isInfixOf` lowered)+ (map fst (_tkuUIDs tk))++sha1 :: B.ByteString -> B.ByteString+sha1 bs = BA.convert (CH.hashWith CHA.SHA1 bs :: CH.Digest CHA.SHA1)++zbase32 :: B.ByteString -> B.ByteString+zbase32 = BC8.pack . encodeZBase32 . B.unpack++encodeZBase32 :: [Word8] -> String+encodeZBase32 = go 0 0+ where+ alphabet = "ybndrfg8ejkmcpqxot1uwisza345h769"+ pick i = alphabet !! i+ go _ 0 [] = []+ go acc bits [] =+ [pick (fromIntegral (((acc `shiftL` (5 - bits)) .&. 31) :: Int))]+ go acc bits (x:xs)+ | bits >= 5 =+ pick (fromIntegral (((acc `shiftR` (bits - 5)) .&. 31) :: Int)) :+ go acc (bits - 5) (x : xs)+ | otherwise = go ((acc `shiftL` 8) .|. fromIntegral x) (bits + 8) xs
− HOpenPGP/Tools/HKP.hs
@@ -1,126 +0,0 @@--- HKP.hs: hOpenPGP key tool--- Copyright © 2016-2026 Clint Adams------ vim: softtabstop=4:shiftwidth=4:expandtab------ This program is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as--- published by the Free Software Foundation, either version 3 of the--- License, or (at your option) any later version.------ This program is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with this program. If not, see <http://www.gnu.org/licenses/>.--{-# LANGUAGE OverloadedStrings #-}--module HOpenPGP.Tools.HKP- ( fetchKeys- , FetchValidationMethod(..)- , rearmorKeys- ) where--import qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA-import Codec.Encryption.OpenPGP.ASCIIArmor.Types- ( Armor(Armor)- , ArmorType(ArmorPublicKeyBlock)- )-import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)-import Codec.Encryption.OpenPGP.Types- ( Block(..)- , TKUnknown(..)- , Fingerprint- )-import Control.Applicative (liftA2)-import Control.Arrow ((&&&))-import Control.Lens ((^..))-import Control.Monad.IO.Class (liftIO)-import Control.Monad.Trans.Except (ExceptT(..), throwE)-import Data.Binary (get, put)-import Data.Binary.Put (runPut)-import qualified Data.ByteString as B-import qualified Data.ByteString.Char8 as BC8-import qualified Data.ByteString.Lazy as BL-import Data.Conduit ((.|), runConduitRes)-import qualified Data.Conduit.Binary as CB-import qualified Data.Conduit.List as CL-import Data.Conduit.OpenPGP.Keyring (conduitToTKsDropping)-import Data.Conduit.Serialization.Binary (conduitGet)-import Data.Data.Lens (biplate)-import Data.Either (rights)-import Data.Monoid ((<>), mempty)-import Data.Time.Clock.POSIX (getPOSIXTime)-import HOpenPGP.Tools.TKUtils (processTK)-import Network.HTTP.Client- ( Response(..)- , httpLbs- , newManager- , parseUrlThrow- , setQueryString- )-import Network.HTTP.Client.TLS (tlsManagerSettings)-import Network.HTTP.Types.Status (ok200)-import Prettyprinter (pretty)--data FetchValidationMethod- = MatchPrimaryKeyFingerprint- | MatchPrimaryOrAnySubkeyFingerprint- | AnySelfSigned- deriving (Bounded, Enum, Eq, Read, Show)--fetchKeys ::- String- -> FetchValidationMethod- -> Fingerprint- -> ExceptT String IO [TKUnknown]-fetchKeys ks fvm q = do- manager <- liftIO $ newManager tlsManagerSettings- request <- liftIO $ parseUrlThrow (ks <> basereq)- let newreq = setQueryString (newqs q) request- response <- liftIO $ httpLbs newreq manager- processedKeys <-- if responseStatus response == ok200- then validateKeys (responseBody response)- else throwE ("HTTP status: " ++ show (responseStatus response))- return $ map fst $ filter (fvp fvm . fst . _tkuKey . snd) processedKeys- where- fvp MatchPrimaryKeyFingerprint k = fingerprint k == q- fvp MatchPrimaryOrAnySubkeyFingerprint k =- any (\k -> fingerprint k == q) (k ^.. biplate)- fvp AnySelfSigned k = True- basereq = "/pks/lookup"- newqs q =- [ ("op", Just "get")- , ("options", Just "mr")- , ("exact", Just "on")- , ("search", Just (BC8.pack ("0x" <> show (pretty q)))) -- FIXME: butter- ]--validateKeys :: BL.ByteString -> ExceptT String IO [(TKUnknown, TKUnknown)] -- FIXME: conduit fail-validateKeys larmors = do- bytestrings <-- ExceptT $ return $ fmap (mconcat . map armorToBS) (AA.decodeLazy larmors)- keys <-- liftIO . runConduitRes $- CB.sourceLbs bytestrings .| conduitGet get .| conduitToTKsDropping .|- CL.consume- cpt <- liftIO getPOSIXTime- return . rights $- map (uncurry (liftA2 (,)) . (pure &&& processTK (Just cpt))) keys- where- armorToBS (Armor ArmorPublicKeyBlock _ bs) = bs- armorToBS _ = mempty--rearmorKeys :: [TKUnknown] -> B.ByteString-rearmorKeys keys =- if null keys- then mempty- else AA.encode .- return .- Armor ArmorPublicKeyBlock [("Comment", "filtered by hokey")] .- runPut . put . Block $- keys
+ HOpenPGP/Tools/Hokey/Canonicalize.hs view
@@ -0,0 +1,57 @@+-- Canonicalize.hs: hOpenPGP key tool canonicalize subcommand+-- Copyright © 2013-2026 Clint Adams+--+-- vim: softtabstop=4:shiftwidth=4:expandtab+--+-- This program is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as+-- published by the Free Software Foundation, either version 3 of the+-- License, or (at your option) any later version.+--+-- This program is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with this program. If not, see <http://www.gnu.org/licenses/>.++module HOpenPGP.Tools.Hokey.Canonicalize+ (+ doCanonicalize+ ) where++import Codec.Encryption.OpenPGP.Serialize ()+import Codec.Encryption.OpenPGP.Types+import Control.Lens (_2, mapped, over)+import Data.Binary (get, put)+import Data.Conduit ((.|), runConduitRes)+import qualified Data.Conduit.Binary as CB+import qualified Data.Conduit.List as CL+import Data.Conduit.OpenPGP.Keyring (conduitToTKsDropping)+import Data.Conduit.Serialization.Binary (conduitGet, conduitPut)+import Data.List (nub, sort)++import System.IO+ ( stdin+ , stdout+ )++doCanonicalize :: IO ()+doCanonicalize =+ runConduitRes $ CB.sourceHandle stdin .| conduitGet get .|+ conduitToTKsDropping .|+ CL.map canonicalize .|+ CL.map put .|+ conduitPut .|+ CB.sinkHandle stdout+ where+ canonicalize tk =+ tk+ { _tkuRevs = sort (_tkuRevs tk)+ , _tkuUIDs = indepthsort (_tkuUIDs tk)+ , _tkuUAts = indepthsort (_tkuUAts tk)+ , _tkuSubs = indepthsort (_tkuSubs tk)+ }+ indepthsort :: (Ord a, Ord b) => [(a, [b])] -> [(a, [b])]+ indepthsort = nub . sort . over (mapped . _2) sort
+ HOpenPGP/Tools/Hokey/Fetch.hs view
@@ -0,0 +1,50 @@+-- Fetch.hs: hOpenPGP key tool fetch subcommand+-- Copyright © 2013-2026 Clint Adams+--+-- vim: softtabstop=4:shiftwidth=4:expandtab+--+-- This program is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as+-- published by the Free Software Foundation, either version 3 of the+-- License, or (at your option) any later version.+--+-- This program is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with this program. If not, see <http://www.gnu.org/licenses/>.++module HOpenPGP.Tools.Hokey.Fetch+ (+ doFetch+ ) where++import Codec.Encryption.OpenPGP.KeySelection (parseFingerprint)+import Codec.Encryption.OpenPGP.Serialize ()+import Control.Monad.Trans.Except (ExceptT(..), runExceptT)+import qualified Data.ByteString as B+import qualified Data.Text as T+import HOpenPGP.Tools.Common.HKP (rearmorKeys)+import qualified HOpenPGP.Tools.Common.HKP as HKP+import qualified HOpenPGP.Tools.Common.WKD as WKD+import HOpenPGP.Tools.Hokey.Options (FetchOptions(..), FetchMethod(..))++import System.IO+ ( hPutStrLn+ , stderr+ )++doFetch :: FetchOptions -> IO ()+doFetch o = do+ ekeys <-+ runExceptT $+ case fetchMethod o of+ HKP -> do+ fp <- ExceptT . return . parseFingerprint . T.pack $ fetchQuery o+ HKP.fetchKeys (keyServer o) (fetchValidation o) fp+ WKD -> WKD.fetchKeys (fetchValidation o) (T.pack (fetchQuery o))+ case ekeys of+ Left e -> hPutStrLn stderr $ "error fetching keys: " ++ e+ Right ks -> B.putStr $ rearmorKeys ks
+ HOpenPGP/Tools/Hokey/InjectSSHAgent.hs view
@@ -0,0 +1,374 @@+-- InjectSSHAgent.hs: hOpenPGP key tool inject-ssh-agent subcommand+-- Copyright © 2013-2026 Clint Adams+--+-- vim: softtabstop=4:shiftwidth=4:expandtab+--+-- This program is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as+-- published by the Free Software Foundation, either version 3 of the+-- License, or (at your option) any later version.+--+-- This program is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with this program. If not, see <http://www.gnu.org/licenses/>.++{-# LANGUAGE GADTs #-}++module HOpenPGP.Tools.Hokey.InjectSSHAgent+ (+ doInjectSSHAgent+ ) where+import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)+import Codec.Encryption.OpenPGP.Ontology+ ( isKUF+ , isSKBindingSig+ )+import Codec.Encryption.OpenPGP.Serialize ()+import Codec.Encryption.OpenPGP.Types+import Control.Exception (bracket)+import qualified Crypto.PubKey.RSA as RSA+import Data.Binary (get)+import Data.Binary.Get (getWord32be, runGet)+import Data.Binary.Put (Put, putByteString, putLazyByteString, putWord32be, putWord8, runPut)+import Data.Bits ((.&.), shiftR, testBit)+import qualified Data.ByteString as B+import qualified Data.ByteString.Base16 as Base16+import qualified Data.ByteString.Char8 as BC8+import qualified Data.ByteString.Lazy as BL+import Data.Conduit ((.|), runConduitRes)+import qualified Data.Conduit.Binary as CB+import qualified Data.Conduit.List as CL+import Data.Conduit.OpenPGP.Keyring (AuthSecretSubkeyAtTime, authSecretSubkeyPrimaryUID, authSecretSubkeyValue, conduitToAuthSecretSubkeysAt, conduitToSecretTKs)+import Data.Conduit.Serialization.Binary (conduitGet)+import Data.Foldable (find)+import Data.List (intercalate, sortOn)+import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)+import Data.Ord (Down(..))+import qualified Data.Set as Set+import Data.Text (Text)+import qualified Data.Text as T+import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime, posixSecondsToUTCTime)+import HOpenPGP.Tools.Common.Common (renderFingerprint)+import Network.Socket (Family(AF_UNIX), SockAddr(..), Socket, SocketType(Stream), close, connect, defaultProtocol, socket)+import qualified Network.Socket.ByteString as NSB+import System.Environment (lookupEnv)+import System.Exit (exitFailure)+import HOpenPGP.Tools.Hokey.Options (InjectSSHAgentOptions(..))+import System.IO+ ( hPutStrLn+ , stderr+ , stdin+ )++doInjectSSHAgent :: InjectSSHAgentOptions -> IO ()+doInjectSSHAgent opts = do+ socketPath <- resolveSSHAgentSocketPath (injectSSHAgentSocket opts)+ input <- readInjectedSecretKeyMaterial opts+ cpt <- getPOSIXTime+ authCandidates <-+ runConduitRes $+ CL.sourceList (BL.toChunks input) .| conduitGet get .| conduitToSecretTKs .|+ conduitToAuthSecretSubkeysAt (posixSecondsToUTCTime cpt) .|+ CL.consume+ injectableCandidates <-+ if null authCandidates+ then inferInjectableAuthSubkeys cpt input+ else pure (map candidateFromAuthSecretSubkey authCandidates)+ whenEmpty injectableCandidates "inject-ssh-agent: no authentication-capable secret subkey found"+ selectedRequests <-+ selectInjectableAuthSubkeys+ (injectSSHAgentComment opts)+ injectableCandidates+ mapM_+ (\(selected, request) -> do+ sendAddIdentityToSSHAgent socketPath request+ hPutStrLn stderr $+ "inject-ssh-agent: added authentication subkey " +++ renderFingerprint (fingerprint (injectableAuthSubkeyPKP selected)) +++ " to ssh-agent")+ selectedRequests++candidateFromAuthSecretSubkey :: AuthSecretSubkeyAtTime -> InjectableAuthSubkey+candidateFromAuthSecretSubkey authSubkey =+ InjectableAuthSubkey+ { injectableAuthSubkeyPKP = authSecretSubkeyPKP authSubkey+ , injectableAuthSubkeySKA = authSecretSubkeySKA authSubkey+ , injectableAuthSubkeyPrimaryUID = authSecretSubkeyPrimaryUID authSubkey+ }++inferInjectableAuthSubkeys ::+ POSIXTime -> BL.ByteString -> IO [InjectableAuthSubkey]+inferInjectableAuthSubkeys _cpt input = do+ tks <-+ runConduitRes $+ CL.sourceList (BL.toChunks input) .| conduitGet get .| conduitToSecretTKs .|+ CL.consume+ pure (concatMap inferFromTK tks)+ where+ inferFromTK tk =+ let mPrimaryUID = fst <$> listToMaybe (_tkUIDs tk)+ in mapMaybe (inferFromSubkey mPrimaryUID) (_tkSubs tk)+ inferFromSubkey :: Maybe Text -> (KeyPkt k, [SignaturePayload]) -> Maybe InjectableAuthSubkey+ inferFromSubkey mPrimaryUID (KeyPktSecretSubkey pkp ska, sigs)+ | hasAuthCapability sigs =+ Just+ InjectableAuthSubkey+ { injectableAuthSubkeyPKP = pkp+ , injectableAuthSubkeySKA = ska+ , injectableAuthSubkeyPrimaryUID = mPrimaryUID+ }+ inferFromSubkey _ _ = Nothing+ hasAuthCapability sigs =+ any+ (Set.member AuthKey)+ (mapMaybe signatureKeyFlags (newestWithUsageFlags (filter isSKBindingSig sigs)))+ newestWithUsageFlags =+ take 1 . sortOn (Down . take 1 . sigCreationTimes) . filter (any isKUF . signatureHashedSubpackets)+ sigCreationTimes = mapMaybe sigCreationTimeFromSubpacket . signatureHashedSubpackets+ sigCreationTimeFromSubpacket (SigSubPacket _ (SigCreationTime ct)) = Just ct+ sigCreationTimeFromSubpacket _ = Nothing+ signatureHashedSubpackets (SigV4 _ _ _ hasheds _ _ _) = hasheds+ signatureHashedSubpackets (SigV6 _ _ _ _ hasheds _ _ _) = hasheds+ signatureHashedSubpackets _ = []+ signatureKeyFlags sig = do+ sp <- find isKUF (signatureHashedSubpackets sig)+ case sp of+ SigSubPacket _ (KeyFlags flags) -> Just flags+ _ -> Nothing++resolveSSHAgentSocketPath :: Maybe String -> IO String+resolveSSHAgentSocketPath (Just path) = pure path+resolveSSHAgentSocketPath Nothing = do+ envPath <- lookupEnv "SSH_AUTH_SOCK"+ case envPath of+ Just path -> pure path+ Nothing ->+ failInject+ "inject-ssh-agent: SSH_AUTH_SOCK is not set; use --ssh-agent-socket"++readInjectedSecretKeyMaterial :: InjectSSHAgentOptions -> IO BL.ByteString+readInjectedSecretKeyMaterial opts = do+ chunks <-+ case injectSSHAgentFromFD opts of+ Nothing -> runConduitRes $ CB.sourceHandle stdin .| CL.consume+ Just fd+ | fd < 0 ->+ failInject "inject-ssh-agent: --from-fd must be a non-negative integer"+ | otherwise ->+ runConduitRes $ CB.sourceFile ("/dev/fd/" ++ show fd) .| CL.consume+ let input = BL.fromChunks chunks+ if BL.null input+ then+ failInject+ "inject-ssh-agent: no secret key bytes were provided on the selected input stream"+ else pure input++selectInjectableAuthSubkeys ::+ Maybe String -> [InjectableAuthSubkey] -> IO [(InjectableAuthSubkey, BL.ByteString)]+selectInjectableAuthSubkeys mComment candidates+ | null selectedRequests =+ failInject+ ("inject-ssh-agent: auth-capable subkeys were found, but none are supported for ssh-agent injection: " +++ intercalate "; " (reverse errs))+ | otherwise = pure (reverse selectedRequests)+ where+ (errs, selectedRequests) = foldl' pick ([], []) candidates+ pick (accErrs, accSelected) candidate =+ case+ sshAddIdentityRequest+ (fromMaybe (defaultSSHComment candidate) mComment)+ (injectableAuthSubkeyPKP candidate)+ (injectableAuthSubkeySKA candidate) of+ Left err -> (err : accErrs, accSelected)+ Right request -> (accErrs, (candidate, request) : accSelected)+ defaultSSHComment candidate =+ case injectableAuthSubkeyPrimaryUID candidate of+ Just uid -> T.unpack uid+ Nothing -> "openpgp:" ++ BC8.unpack (Base16.encode (BL.toStrict (unFingerprint (fingerprint (injectableAuthSubkeyPKP candidate)))))++sshAddIdentityRequest ::+ String -> SomePKPayload -> SKAddendum -> Either String BL.ByteString+sshAddIdentityRequest comment subkeyPKP subkeySKA =+ case subkeySKA of+ SUUnencrypted (RSAPrivateKey (RSA_PrivateKey rsaPrivateKey)) _ ->+ Right $ frameSSHAgentRequest (rsaAddIdentityPayload (BC8.pack comment) rsaPrivateKey)+ SUUnencrypted (EdDSAPrivateKey Ed25519 secretSeed) _ ->+ frameSSHAgentRequest <$>+ ed25519AddIdentityPayload (BC8.pack comment) subkeyPKP secretSeed+ SUUnencrypted (UnknownSKey rawSecret) _+ | isEd25519PKA (_pkalgo subkeyPKP) ->+ frameSSHAgentRequest <$>+ ed25519AddIdentityPayload+ (BC8.pack comment)+ subkeyPKP+ (BL.toStrict rawSecret)+ SUUnencrypted (EdDSAPrivateKey Ed448 _) _ ->+ Left+ ("subkey " ++ renderFingerprint (fingerprint subkeyPKP) +++ " uses Ed448, which is not supported by ssh-agent add-identity")+ SUUnencrypted _ _ ->+ Left+ ("subkey " ++ renderFingerprint (fingerprint subkeyPKP) +++ " uses an unsupported key algorithm for ssh-agent injection")+ _ ->+ Left+ ("subkey " ++ renderFingerprint (fingerprint subkeyPKP) +++ " is encrypted; decrypt it before injection")++authSecretSubkeyPKP :: AuthSecretSubkeyAtTime -> SomePKPayload+authSecretSubkeyPKP = keyPktPKPayload . authSecretSubkeyValue++authSecretSubkeySKA :: AuthSecretSubkeyAtTime -> SKAddendum+authSecretSubkeySKA = secretKeyPktSKAddendum . authSecretSubkeyValue++rsaAddIdentityPayload :: B.ByteString -> RSA.PrivateKey -> BL.ByteString+rsaAddIdentityPayload comment privateKey =+ runPut $ do+ putWord8 17+ putSSHString (BC8.pack "ssh-rsa")+ putSSHMpint (RSA.public_n (RSA.private_pub privateKey))+ putSSHMpint (RSA.public_e (RSA.private_pub privateKey))+ putSSHMpint (RSA.private_d privateKey)+ putSSHMpint (RSA.private_qinv privateKey)+ putSSHMpint (RSA.private_p privateKey)+ putSSHMpint (RSA.private_q privateKey)+ putSSHString comment++ed25519AddIdentityPayload ::+ B.ByteString -> SomePKPayload -> B.ByteString -> Either String BL.ByteString+ed25519AddIdentityPayload comment pkp rawSecret = do+ publicKey <- ed25519PublicPoint pkp+ secretSeed <- normalizeEd25519Secret rawSecret+ pure $+ runPut $ do+ putWord8 17+ putSSHString (BC8.pack "ssh-ed25519")+ putSSHString publicKey+ putSSHString (secretSeed <> publicKey)+ putSSHString comment++ed25519PublicPoint :: SomePKPayload -> Either String B.ByteString+ed25519PublicPoint pkp =+ case _pubkey pkp of+ EdDSAPubKey Ed25519 point ->+ maybe+ (Left ("invalid Ed25519 public point for subkey " ++ renderFingerprint (fingerprint pkp)))+ Right+ (edPointToRawBytes point)+ _ -> Left ("subkey " ++ renderFingerprint (fingerprint pkp) ++ " does not have an Ed25519 public key")++edPointToRawBytes :: EdPoint -> Maybe B.ByteString+edPointToRawBytes (NativeEPoint (EPoint i)) = integerToFixedBytes 32 i+edPointToRawBytes (PrefixedNativeEPoint (EPoint i)) = do+ prefixed <- integerToFixedBytes 33 i+ case B.uncons prefixed of+ Just (0x40, raw) -> Just raw+ _ -> Nothing++normalizeEd25519Secret :: B.ByteString -> Either String B.ByteString+normalizeEd25519Secret rawSecret+ | B.length rawSecret == 32 = Right rawSecret+ | otherwise =+ Left+ ("expected 32-byte Ed25519 secret seed, got " ++ show (B.length rawSecret) ++ " bytes")++putSSHString :: B.ByteString -> Put+putSSHString bs = putWord32be (fromIntegral (B.length bs)) >> putByteString bs++putSSHMpint :: Integer -> Put+putSSHMpint n+ | n <= 0 = putWord32be 0+ | otherwise = putSSHString encoded+ where+ raw = integerToUnsignedBytes n+ encoded =+ case B.uncons raw of+ Just (firstByte, _)+ | testBit firstByte 7 -> B.cons 0x00 raw+ _ -> raw++integerToFixedBytes :: Int -> Integer -> Maybe B.ByteString+integerToFixedBytes width n+ | n < 0 = Nothing+ | B.length raw > width = Nothing+ | otherwise = Just (B.replicate (width - B.length raw) 0x00 <> raw)+ where+ raw =+ if n == 0+ then B.singleton 0x00+ else integerToUnsignedBytes n++integerToUnsignedBytes :: Integer -> B.ByteString+integerToUnsignedBytes n =+ B.reverse $+ B.unfoldr+ (\value ->+ if value == 0+ then Nothing+ else Just (fromIntegral (value .&. 0xff), value `shiftR` 8))+ n++isEd25519PKA :: PubKeyAlgorithm -> Bool+isEd25519PKA pka = fromFVal pka == 27++frameSSHAgentRequest :: BL.ByteString -> BL.ByteString+frameSSHAgentRequest body =+ runPut $ putWord32be (fromIntegral (BL.length body)) >> putLazyByteString body++sendAddIdentityToSSHAgent :: FilePath -> BL.ByteString -> IO ()+sendAddIdentityToSSHAgent socketPath request =+ bracket+ (socket AF_UNIX Stream defaultProtocol)+ close+ (\sock -> do+ connect sock (SockAddrUnix socketPath)+ NSB.sendAll sock (BL.toStrict request)+ response <- readSSHAgentPacket sock+ case B.uncons response of+ Just (6, _) -> pure ()+ Just (5, _) ->+ failInject "inject-ssh-agent: ssh-agent rejected the supplied key"+ Just (code, _) ->+ failInject+ ("inject-ssh-agent: ssh-agent returned unexpected response type " +++ show code)+ Nothing ->+ failInject+ "inject-ssh-agent: ssh-agent returned an empty response packet")++readSSHAgentPacket :: Socket -> IO B.ByteString+readSSHAgentPacket sock = do+ lenPrefix <- recvExact sock 4+ let packetLen = fromIntegral (runGet getWord32be (BL.fromStrict lenPrefix))+ recvExact sock packetLen++recvExact :: Socket -> Int -> IO B.ByteString+recvExact _ 0 = pure B.empty+recvExact sock remaining = go B.empty remaining+ where+ go acc 0 = pure acc+ go acc bytesRemaining = do+ chunk <- NSB.recv sock bytesRemaining+ if B.null chunk+ then+ failInject+ "inject-ssh-agent: ssh-agent socket closed while reading response"+ else go (acc <> chunk) (bytesRemaining - B.length chunk)++whenEmpty :: [a] -> String -> IO ()+whenEmpty [] msg = failInject msg+whenEmpty _ _ = pure ()++failInject :: String -> IO a+failInject msg = hPutStrLn stderr msg >> exitFailure++data InjectableAuthSubkey =+ InjectableAuthSubkey+ { injectableAuthSubkeyPKP :: SomePKPayload+ , injectableAuthSubkeySKA :: SKAddendum+ , injectableAuthSubkeyPrimaryUID :: Maybe Text+ }
+ HOpenPGP/Tools/Hokey/Lint.hs view
@@ -0,0 +1,710 @@+-- Lint.hs: hOpenPGP key tool lint subcommand+-- Copyright © 2013-2026 Clint Adams+--+-- vim: softtabstop=4:shiftwidth=4:expandtab+--+-- This program is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as+-- published by the Free Software Foundation, either version 3 of the+-- License, or (at your option) any later version.+--+-- This program is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with this program. If not, see <http://www.gnu.org/licenses/>.++{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeApplications #-}++module HOpenPGP.Tools.Hokey.Lint+ (+ doLint+ ) where++import Codec.Encryption.OpenPGP.Expirations (getKeyExpirationTimesFromSignature)+import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)+import Codec.Encryption.OpenPGP.KeyInfo (pkalgoAbbrev, pubkeySize)+import Codec.Encryption.OpenPGP.Ontology+ ( isCT+ , isCertRevocationSig+ , isKUF+ , isPHA+ , isPKBindingSig+ , isSKBindingSig+ )+import Codec.Encryption.OpenPGP.Serialize ()+import Codec.Encryption.OpenPGP.Types+import Control.Arrow ((***))+import Control.Error.Util (hush)+import Control.Lens ((&), (^.), _1)+import Control.Monad.Trans.Writer.Lazy (execWriter, tell)+import qualified Crypto.Hash as CH+import qualified Crypto.Hash.Algorithms as CHA+import qualified Data.Aeson as A+import Data.Binary (get)+import qualified Data.ByteArray as BA+import qualified Data.ByteString as B+import qualified Data.ByteString.Base16 as Base16+import qualified Data.ByteString.Char8 as BC8+import qualified Data.ByteString.Lazy as BL+import Data.Conduit ((.|), runConduitRes)+import qualified Data.Conduit.Binary as CB+import qualified Data.Conduit.List as CL+import Data.Conduit.OpenPGP.Keyring (conduitToTKsDropping)+import Data.Conduit.Serialization.Binary (conduitGet)+import Data.Foldable (find)+import Data.List (elemIndex, findIndex, intercalate, sortOn)+import qualified Data.Map as Map+import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)+import Data.Ord (Down(..))+import qualified Data.Set as Set+import Data.Text (Text)+import qualified Data.Text as T+import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime, posixSecondsToUTCTime)+import Data.Time.Format (formatTime)+import qualified Data.Yaml as Y+import GHC.Generics+import HOpenPGP.Tools.Common.Common (renderKeyID, renderFingerprint)+import HOpenPGP.Tools.Common.TKUtils (processTK)+import HOpenPGP.Tools.Hokey.Options (LintOptions(..), LintOutputFormat(..))++import Data.Time.Locale.Compat (defaultTimeLocale)+import System.IO+ ( stdin+ )++import Prettyprinter+ ( Doc+ , (<+>)+ , annotate+ , colon+ , flatAlt+ , indent+ , line+ , list+ , pretty+ )+import qualified Prettyprinter.Render.Terminal as PPA++linebreak :: Doc ann+linebreak = flatAlt line mempty++green, yellow, red :: Doc PPA.AnsiStyle -> Doc PPA.AnsiStyle+green = annotate (PPA.color PPA.Green)+yellow = annotate (PPA.color PPA.Yellow)+red = annotate (PPA.color PPA.Red)++data KAS =+ KAS+ { pubkeyalgo :: Colored PubKeyAlgorithm+ , pubkeysize :: Colored (Maybe Int)+ , stringrep :: String+ }+ deriving (Generic)++data Color+ = Green+ | Yellow+ | Red+ deriving (Eq, Generic, Ord)++data Colored a =+ Colored+ { color :: Maybe Color+ , explanation :: Maybe String+ , val :: a+ }+ deriving (Functor, Generic)++newtype FakeMap a b =+ FakeMap+ { unFakeMap :: [(a, b)]+ }++data KeyReport =+ KeyReport+ { keyStatus :: String+ , keyFingerprint :: Fingerprint+ , keyVer :: Colored KeyVersion+ , keyCreationTime :: ThirtyTwoBitTimeStamp+ , keyAlgorithmAndSize :: KAS+ , keyUIDsAndUAts :: FakeMap Text (Colored UIDReport)+ , keyBestOf :: Maybe UIDReport+ , keySubkeys :: [SubkeyReport]+ , keyHasEncryptionCapableSubkey :: Colored Bool+ }+ deriving (Generic)++data UIDReport =+ UIDReport+ { uidSelfSigHashAlgorithms :: [Colored HashAlgorithm]+ , uidPreferredHashAlgorithms :: [Colored [HashAlgorithm]]+ , uidKeyExpirationTimes :: [Colored [ThirtyTwoBitDuration]]+ , uidKeyUsageFlags :: [Colored (Set.Set KeyFlag)]+ , uidRevocationStatus :: [RevocationStatus]+ }+ deriving (Generic)++data SubkeyReport =+ SubkeyReport+ { skFingerprint :: Colored Fingerprint+ , skVer :: Colored KeyVersion+ , skCreationTime :: ThirtyTwoBitTimeStamp+ , skAlgorithmAndSize :: KAS+ , skBindingSigHashAlgorithms :: [Colored HashAlgorithm]+ , skRevocationSigWeakDigests :: [SubkeyRevocationDigestWarning]+ , skUsageFlags :: [Colored (Set.Set KeyFlag)]+ , skCrossCerts :: CrossCertReport+ }+ deriving (Generic)++data SubkeyRevocationDigestWarning =+ SubkeyRevocationDigestWarning+ { srwHashAlgorithm :: HashAlgorithm+ , srwSubkeyFingerprint :: String+ , srwSubkeyKeyID :: Maybe String+ , srwMessage :: String+ }+ deriving (Generic)++data CrossCertReport =+ CrossCertReport+ { ccPresent :: Colored Bool+ , ccHashAlgorithms :: [Colored HashAlgorithm]+ }+ deriving (Generic)++data RevocationStatus =+ RevocationStatus+ { isRevoked :: Bool+ , revocationCode :: String+ , revocationReason :: Text+ }+ deriving (Generic)++instance A.ToJSON KAS++instance A.ToJSON Color++instance (A.ToJSON a) => A.ToJSON (Colored a)++instance A.ToJSON KeyReport++instance A.ToJSON UIDReport++instance A.ToJSON SubkeyReport++instance A.ToJSON SubkeyRevocationDigestWarning++instance A.ToJSON CrossCertReport++instance A.ToJSON b => A.ToJSON (FakeMap Text b) where+ toJSON = A.toJSON . Map.fromList . unFakeMap++instance A.ToJSON RevocationStatus++instance Semigroup UIDReport where+ (<>) (UIDReport a b c d e) (UIDReport a' b' c' d' e') =+ UIDReport (a <> a') (b <> b') (c <> c') (d <> d') (e <> e')++instance Monoid UIDReport where+ mempty = UIDReport [] [] [] [] []+ mappend = (<>)++checkKey :: Maybe POSIXTime -> TKUnknown -> KeyReport+checkKey mpt key =+ (\x ->+ x+ { keyBestOf = populateBestOf x+ , keyHasEncryptionCapableSubkey =+ hasEncryptionCapableSubkey (concatMap skUsageFlags (keySubkeys x))+ })+ KeyReport+ { keyStatus = either id (const "good") processedTK+ , keyFingerprint = key ^. tkuKey . _1 & fingerprint+ , keyVer = key ^. tkuKey . _1 & _keyVersion & colorizeKV+ , keyCreationTime = key ^. tkuKey . _1 & _timestamp+ , keyAlgorithmAndSize = kasIt (key ^. tkuKey . _1)+ , keyUIDsAndUAts =+ FakeMap+ (map (\(x, y) -> (x, uidr (Just x) y)) (processedOrOrig ^. tkuUIDs) +++ map (uatspsToText *** uidr Nothing) (processedOrOrig ^. tkuUAts))+ , keyBestOf = Nothing+ , keySubkeys =+ map (checkSK (key ^. tkuKey . _1 & fingerprint)) (key ^. tkuSubs)+ , keyHasEncryptionCapableSubkey = Colored Nothing Nothing False+ }+ where+ processedOrOrig = either (const key) id processedTK+ processedTK = processTK mpt key+ kasIt :: SomePKPayload -> KAS+ kasIt pkp = kasIt' (_pkalgo pkp) (_pubkey pkp & pubkeySize)+ kasIt' :: PubKeyAlgorithm -> Either String Int -> KAS+ kasIt' pka epks =+ KAS+ { pubkeyalgo = colorizePKA pka+ , pubkeysize = colorizePKS pka epks+ , stringrep = (either (const "unknown") show epks) ++ (pkalgoAbbrev pka)+ }+ colorizeKV kv =+ uncurry+ Colored+ (if kv == V4+ then (Just Green, Nothing)+ else (Just Red, Just "not a V4 key"))+ kv+ colorizePKA pka+ | pka `elem` [RSA, EdDSA, ECDH] = Colored (Just Green) Nothing pka+ | otherwise =+ Colored+ (Just Yellow)+ (Just "public key algorithm neither RSA nor EdDSA")+ pka+ colorizePKS pka epks = uncurry Colored (colorizePKS' pka epks) (hush epks)+ colorizePKS' pka (Right pks)+ | pka `elem` [EdDSA, ECDH] && pks >= 256 = (Just Green, Nothing)+ | pka `elem` [EdDSA, ECDH] =+ (Just Yellow, Just "Public key size under 256 bits")+ | pka == RSA && pks >= 3072 = (Just Green, Nothing)+ | pka == RSA && pks >= 2048 =+ (Just Yellow, Just "Public key size between 2048 and 3072 bits")+ | pka == RSA = (Just Red, Just "Public key size under 2048 bits")+ | otherwise = (Nothing, Nothing)+ colorizePKS' _ (Left _) =+ (Just Red, Just "public key algorithm not understood")+ colorizePHAs x =+ uncurry+ Colored+ (if preferredWeakHash x+ then (Just Red, Just "weak hash with higher preference")+ else (Just Green, Nothing))+ x+ fSHA2Family = fi (`elem` [SHA512, SHA384, SHA256, SHA224])+ firstStrongSHA2 xs = fSHA2Family xs+ preferredWeakHash xs =+ any+ (\ha -> fromMaybe maxBound (elemIndex ha xs) < firstStrongSHA2 xs)+ knownWeakHashAlgorithms+ fi x y = fromMaybe maxBound (findIndex x y)+ colorizeKETs ct ts kes+ | null kes = Colored (Just Red) (Just "no expiration set") kes+ | any (\ke -> realToFrac ts + realToFrac ke < ct) kes =+ Colored (Just Red) (Just "expiration passed") kes+ | any (\ke -> realToFrac ts + realToFrac ke > ct + (5 * 31557600)) kes =+ Colored (Just Yellow) (Just "expiration too far in future") kes+ | otherwise = Colored (Just Green) Nothing kes+ eoki pkp+ | _keyVersion pkp == V4 = hush . eightOctetKeyID $ pkp+ | _keyVersion pkp == DeprecatedV3 &&+ elem (_pkalgo pkp) [RSA, DeprecatedRSASignOnly] =+ hush . eightOctetKeyID $ pkp+ | otherwise = Nothing+ phas (SigV4 _ _ _ xs _ _ _) =+ colorizePHAs .+ concatMap (\(SigSubPacket _ (PreferredHashAlgorithms x)) -> x) $+ filter isPHA xs+ phas _ = Colored Nothing Nothing []+ has = map (colorizeHA . hashAlgo) . alleged+ colorizeHA ha =+ uncurry+ Colored+ (if isKnownWeakHashAlgorithm ha+ then (Just Red, Just "weak hash algorithm")+ else (Nothing, Nothing))+ ha+ sigcts (SigV4 _ _ _ xs _ _ _) =+ map (\(SigSubPacket _ (SigCreationTime x)) -> x) $ filter isCT xs+ sigcts (SigV6 _ _ _ _ xs _ _ _) =+ map (\(SigSubPacket _ (SigCreationTime x)) -> x) $ filter isCT xs+ alleged =+ filter+ (\x -> ((==) <$> sigissuer x <*> eoki (key ^. tkuKey . _1)) == Just True)+ uatspsToText = T.pack . uatspsToString+ uatspsToString us =+ "<uat:[" ++ intercalate "," (map uaspToString us) ++ "]>"+ uaspToString (ImageAttribute hdr d) =+ hdrToString hdr ++ ':' : show (BL.length d) ++ ':' :+ BC8.unpack (Base16.encode (BA.convert (CH.hashlazy @CHA.SHA3_512 d)))+ uaspToString (OtherUASub t d) =+ "other-" ++ show t ++ ':' : show (BL.length d) ++ ':' :+ BC8.unpack (Base16.encode (BA.convert (CH.hashlazy @CHA.SHA3_512 d)))+ hdrToString (ImageHV1 JPEG) = "jpeg"+ hdrToString (ImageHV1 fmt) = "image-" ++ show (fromFVal fmt)+ uidr Nothing sps =+ Colored+ Nothing+ Nothing+ (UIDReport+ (has sps)+ (map phas sps)+ (map+ (colorizeKETs+ (fromMaybe 0 mpt)+ (unThirtyTwoBitTimeStamp (_timestamp (key ^. tkuKey . _1))) .+ getKeyExpirationTimesFromSignature)+ sps -- should that be 0?+ )+ (kufs False sps)+ (findRevocationReason sps))+ uidr (Just u) sps =+ colorizeUID+ u+ (UIDReport+ (has sps)+ (map phas sps)+ (map+ (colorizeKETs+ (fromMaybe 0 mpt)+ (unThirtyTwoBitTimeStamp (_timestamp (key ^. tkuKey . _1))) .+ getKeyExpirationTimesFromSignature)+ sps -- should that be 0?+ )+ (kufs False sps)+ (findRevocationReason sps))+ populateBestOf krep =+ Just+ (UIDReport <$> best . uidSelfSigHashAlgorithms <*> best .+ uidPreferredHashAlgorithms <*>+ best .+ uidKeyExpirationTimes <*>+ best .+ uidKeyUsageFlags <*>+ pure [] $+ mconcat (justTheUIDRs krep))+ justTheUIDRs = map (decolorize . snd) . unFakeMap . keyUIDsAndUAts+ best = take 1 . sortOn color+ decolorize (Colored _ _ x) = x+ colorizeUID u+ | '(' `elem` T.unpack u =+ Colored (Just Yellow) (Just "parenthesis in uid") -- FIXME: be more discerning+ | '<' `notElem` T.unpack u =+ Colored (Just Yellow) (Just "no left angle bracket in uid") -- FIXME: be more discerning+ | otherwise = Colored Nothing Nothing+ findRevocationReason = concatMap grabReasons . filter isCertRevocationSig+ grabReasons (SigV4 CertRevocationSig _ _ has _ _ _) =+ mapMaybe (grabReasons' . _sspPayload) has+ grabReasons _ = []+ grabReasons' (ReasonForRevocation a b) =+ Just (RevocationStatus True (show a) b)+ grabReasons' _ = Nothing+ kufs s =+ map+ (colorizeKUFs s . (\(SigSubPacket _ (KeyFlags x)) -> x) .+ fromMaybe undefined .+ find isKUF .+ hasheds) .+ newestWith (any isKUF . hasheds)+ colorizeKUFs False x =+ uncurry+ Colored+ (if (Set.member EncryptStorageKey x ||+ Set.member EncryptCommunicationsKey x) &&+ (Set.member SignDataKey x || Set.member CertifyKeysKey x)+ then (Just Yellow, Just "both signing & encryption")+ else (Just Green, Nothing))+ x+ colorizeKUFs True x =+ uncurry+ Colored+ (if Set.member CertifyKeysKey x+ then (Just Red, Just "certification-capable subkey")+ else (if (Set.member EncryptStorageKey x ||+ Set.member EncryptCommunicationsKey x) &&+ Set.member SignDataKey x+ then (Just Yellow, Just "both signing & encryption")+ else (Just Green, Nothing)))+ x+ newestWith p = take 1 . sortOn (Down . take 1 . sigcts) . filter p -- FIXME: this is terrible+ hasheds (SigV4 _ _ _ xs _ _ _) = xs+ hasheds (SigV6 _ _ _ _ xs _ _ _) = xs+ hasheds _ = []+ checkSK ::+ Fingerprint -> (Pkt, [SignaturePayload]) -> SubkeyReport+ checkSK pf (PublicSubkeyPkt pkp, sigs) = checkSK' pf pkp sigs+ checkSK pf (SecretSubkeyPkt pkp _, sigs) = checkSK' pf pkp sigs+ checkSK' pf pkp sigs =+ (\x -> x {skCrossCerts = ccr (map decolorize (skUsageFlags x)) sigs})+ SubkeyReport+ { skFingerprint = colorizeF pf (fingerprint pkp)+ , skVer = colorizeKV (_keyVersion pkp)+ , skCreationTime = _timestamp pkp+ , skAlgorithmAndSize = kasIt pkp+ , skBindingSigHashAlgorithms = has (filter isSKBindingSig sigs)+ , skRevocationSigWeakDigests =+ subkeyRevocationSigWeakDigests pkp sigs+ , skUsageFlags = kufs True (filter isSKBindingSig sigs)+ , skCrossCerts = CrossCertReport (Colored Nothing Nothing False) []+ }+ hasEncryptionCapableSubkey skrs =+ if any+ ((\x ->+ Set.member EncryptStorageKey x ||+ Set.member EncryptCommunicationsKey x) .+ decolorize)+ skrs+ then Colored (Just Green) Nothing True+ else Colored+ (Just Red)+ (Just "no encryption-capable subkey present")+ False+ embeddedSigs =+ filter isPKBindingSig . concatMap getEmbeds . filter isSKBindingSig+ getEmbeds (SigV4 _ _ _ xs ys _ _) = concatMap getEmbed (xs ++ ys)+ getEmbeds _ = []+ getEmbed (SigSubPacket _ (EmbeddedSignature sp)) = [sp]+ getEmbed _ = []+ ccr kufs sigs =+ CrossCertReport (colorES kufs sigs) (map (colorizeHA . hashAlgo) sigs)+ colorES kufs sigs =+ case ( null (embeddedSigs sigs)+ , any (Set.member SignDataKey) kufs+ , any (Set.member AuthKey) kufs) of+ (True, True, True) ->+ Colored+ (Just Red)+ (Just "signing- and auth-capable subkey without cross-cert")+ False+ (True, True, False) ->+ Colored+ (Just Red)+ (Just "signing-capable subkey without cross-cert")+ False+ (True, False, True) ->+ Colored+ (Just Yellow)+ (Just "auth-capable subkey without cross-cert")+ False+ (False, True, True) -> Colored (Just Green) Nothing True+ (False, True, False) -> Colored (Just Green) Nothing True+ (False, False, True) -> Colored (Just Green) Nothing True+ (False, False, False) -> Colored Nothing Nothing True+ (True, _, _) -> Colored Nothing Nothing False+ colorizeF pf fp =+ uncurry+ Colored+ (if pf == fp+ then (Just Red, Just "subkey has same fingerprint as primary key")+ else (Just Green, Nothing))+ fp+ subkeyRevocationSigWeakDigests pkp =+ mapMaybe (mkSubkeyRevocationSigWeakDigestWarning pkp) .+ filter isSubkeyRevocationSignature+ mkSubkeyRevocationSigWeakDigestWarning pkp sig =+ let ha = hashAlgo sig+ in if isKnownWeakHashAlgorithm ha+ then+ Just+ SubkeyRevocationDigestWarning+ { srwHashAlgorithm = ha+ , srwSubkeyFingerprint = renderFingerprint (fingerprint pkp)+ , srwSubkeyKeyID = fmap renderKeyID (hush (eightOctetKeyID pkp))+ , srwMessage =+ "subkey revocation signature uses known-weak digest algorithm"+ }+ else Nothing++prettyKeyReport :: POSIXTime -> TKUnknown -> Doc PPA.AnsiStyle+prettyKeyReport cpt key = do+ let keyReport = checkKey (Just cpt) key+ execWriter $+ tell+ (linebreak <> pretty "Key has potential validity" <> colon <+>+ pretty (keyStatus keyReport) <>+ linebreak <>+ pretty "Key has fingerprint" <>+ colon <+>+ pretty (SpacedFingerprint (keyFingerprint keyReport)) <>+ linebreak <>+ pretty "Checking to see if key is OpenPGPv4" <>+ colon <+>+ coloredToColor (pretty . show) (keyVer keyReport) <>+ linebreak <>+ (\kas ->+ pretty "Checking the strength of your primary asymmetric key" <> colon <+>+ coloredToColor pretty (pubkeyalgo kas) <+>+ coloredToColor (maybe (pretty "unknown") pretty) (pubkeysize kas))+ (keyAlgorithmAndSize keyReport) <>+ linebreak <>+ pretty "Checking user-ID- and user-attribute-related items" <>+ colon <>+ mconcat+ (map+ (uidtrip (keyCreationTime keyReport) . gottabeabetterway)+ (unFakeMap (keyUIDsAndUAts keyReport))) <>+ linebreak <>+ pretty "Checking subkeys" <>+ colon <>+ linebreak <>+ indent+ 2+ (pretty "one of the subkeys is encryption-capable" <> colon <+>+ coloredToColor pretty (keyHasEncryptionCapableSubkey keyReport)) <>+ mconcat (map subkeyrep (keySubkeys keyReport)) <>+ linebreak)+ where+ coloredToColor f (Colored (Just Green) _ x) = green (f x)+ coloredToColor f (Colored (Just Yellow) _ x) = yellow (f x)+ coloredToColor f (Colored (Just Red) _ x) = red (f x)+ coloredToColor f (Colored Nothing _ x) = f x+ uidtrip ts (u, ur)+ | null (uidRevocationStatus ur) =+ linebreak <> indent 2 (coloredToColor pretty (fmap T.unpack u)) <> colon <>+ linebreak <>+ indent+ 4+ (pretty "Self-sig hash algorithms" <> colon <+>+ (list . map (coloredToColor pretty) . uidSelfSigHashAlgorithms) ur) <>+ linebreak <>+ indent+ 4+ (pretty "Preferred hash algorithms" <> colon <+>+ mconcat (map (coloredToColor pretty) (uidPreferredHashAlgorithms ur))) <>+ linebreak <>+ indent+ 4+ (pretty "Key expiration times" <> colon <+>+ mconcat+ (map+ (coloredToColor list . fmap (map (pretty . keyExp ts)))+ (uidKeyExpirationTimes ur))) <>+ linebreak <>+ indent+ 4+ (pretty "Key usage flags" <> colon <+>+ (list . map (coloredToColor (pretty . Set.toList)))+ (uidKeyUsageFlags ur))+ | otherwise =+ linebreak <> indent 2 (coloredToColor pretty (fmap T.unpack u)) <> colon <+>+ pretty "[revoked]" <>+ linebreak <>+ indent+ 4+ (pretty "Revocation code" <> colon <+>+ list (map (pretty . revocationCode) (uidRevocationStatus ur))) <>+ linebreak <>+ indent+ 4+ (pretty "Revocation reason" <> colon <+>+ list+ (map+ (pretty . T.unpack . revocationReason)+ (uidRevocationStatus ur)))+ keyExp ts ke =+ (show . pretty) ke ++ " = " +++ formatTime+ defaultTimeLocale+ "%c"+ (posixSecondsToUTCTime (realToFrac ts + realToFrac ke))+ gottabeabetterway (a, Colored x y z) = (Colored x y a, z)+ subkeyrep skr =+ linebreak <>+ indent+ 2+ (pretty "fpr" <> colon <+>+ coloredToColor pretty (fmap SpacedFingerprint (skFingerprint skr))) <>+ linebreak <>+ indent 4 (pretty "version" <> colon <+> coloredToColor pretty (skVer skr)) <>+ linebreak <>+ indent 4 (pretty "timestamp" <> colon <+> pretty (skCreationTime skr)) <>+ linebreak <>+ indent+ 4+ ((\kas ->+ pretty "algo/size" <> colon <+>+ coloredToColor pretty (pubkeyalgo kas) <+>+ coloredToColor (maybe (pretty "unknown") pretty) (pubkeysize kas))+ (skAlgorithmAndSize skr)) <>+ linebreak <>+ indent+ 4+ (pretty "binding sig hash algorithms" <> colon <+>+ (list . map (coloredToColor pretty) . skBindingSigHashAlgorithms) skr) <>+ linebreak <>+ indent+ 4+ (pretty "weak subkey revocation digests" <> colon <+>+ if null (skRevocationSigWeakDigests skr)+ then pretty "[]"+ else+ list+ (map+ (\w ->+ red+ (pretty (srwHashAlgorithm w) <> colon <+>+ pretty (srwSubkeyFingerprint w) <> colon <+>+ maybe (pretty "<no-key-id>") pretty (srwSubkeyKeyID w)))+ (skRevocationSigWeakDigests skr))) <>+ linebreak <>+ indent+ 4+ (pretty "usage flags" <> colon <+>+ (list . map (coloredToColor (pretty . Set.toList))) (skUsageFlags skr)) <>+ linebreak <>+ indent+ 4+ (pretty "embedded cross-cert" <> colon <+>+ (coloredToColor pretty . ccPresent . skCrossCerts) skr) <>+ linebreak <>+ indent+ 4+ (pretty "cross-cert hash algorithms" <> colon <+>+ (list . map (coloredToColor pretty) . ccHashAlgorithms . skCrossCerts)+ skr)++jsonReport :: POSIXTime -> TKUnknown -> BL.ByteString+jsonReport ps = A.encode . checkKey (Just ps)++yamlReport :: POSIXTime -> TKUnknown -> B.ByteString+yamlReport ps = Y.encode . (: []) . checkKey (Just ps)++doLint :: LintOptions -> IO ()+doLint o = do+ cpt <- getPOSIXTime+ keys <-+ runConduitRes $ CB.sourceHandle stdin .| conduitGet get .|+ conduitToTKsDropping .|+ CL.consume+ output (lintOutputFormat o) cpt keys+ where+ output Pretty cpt = mapM_ (PPA.putDoc . prettyKeyReport cpt)+ output JSON cpt =+ mapM_ (BL.putStr . flip BL.append (BL.singleton 0x0a) . jsonReport cpt)+ output YAML cpt = mapM_ (B.putStr . yamlReport cpt)++sigissuer :: SignaturePayload -> Maybe EightOctetKeyId+getIssuer :: SigSubPacketPayload -> Maybe EightOctetKeyId+hashAlgo :: SignaturePayload -> HashAlgorithm+sigissuer (SigVOther 2 _) = Nothing+sigissuer SigV3 {} = Nothing+sigissuer (SigV4 _ _ _ ys xs _ _) =+ listToMaybe . mapMaybe (getIssuer . _sspPayload) $ (ys ++ xs) -- FIXME: what should this be if there are multiple matches?+sigissuer (SigV6 _ _ _ _ ys xs _ _) =+ listToMaybe . mapMaybe (getIssuer . _sspPayload) $ (ys ++ xs) -- FIXME: what should this be if there are multiple matches?+sigissuer (SigVOther _ _) = Nothing++getIssuer (Issuer i) = Just i+getIssuer _ = Nothing++hashAlgo (SigV3 _ _ _ _ x _ _) = x+hashAlgo (SigV4 _ _ x _ _ _ _) = x+hashAlgo (SigV6 _ _ x _ _ _ _ _) = x+hashAlgo (SigVOther _ _) = OtherHA 0++knownWeakHashAlgorithms :: [HashAlgorithm]+knownWeakHashAlgorithms = [DeprecatedMD5, SHA1, RIPEMD160]++isKnownWeakHashAlgorithm :: HashAlgorithm -> Bool+isKnownWeakHashAlgorithm ha = ha `elem` knownWeakHashAlgorithms++isSubkeyRevocationSignature :: SignaturePayload -> Bool+isSubkeyRevocationSignature (SigV3 st _ _ _ _ _ _) = st == SubkeyRevocationSig+isSubkeyRevocationSignature (SigV4 st _ _ _ _ _ _) = st == SubkeyRevocationSig+isSubkeyRevocationSignature (SigV6 st _ _ _ _ _ _ _) = st == SubkeyRevocationSig+isSubkeyRevocationSignature _ = False++
+ HOpenPGP/Tools/Hokey/Options.hs view
@@ -0,0 +1,146 @@+-- Options.hs: hOpenPGP key tool command-line options+-- Copyright © 2013-2026 Clint Adams+--+-- vim: softtabstop=4:shiftwidth=4:expandtab+--+-- This program is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as+-- published by the Free Software Foundation, either version 3 of the+-- License, or (at your option) any later version.+--+-- This program is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with this program. If not, see <http://www.gnu.org/licenses/>.++module HOpenPGP.Tools.Hokey.Options+ (+ -- CanonicalizeOptions+ FetchOptions(..)+ , InjectSSHAgentOptions(..)+ , LintOptions(..)+ , fetchO+ , injectSSHAgentO+ , lintO+ , FetchMethod(..)+ , LintOutputFormat(..)+ ) where++import Codec.Encryption.OpenPGP.Serialize ()+import Control.Applicative (optional)+import HOpenPGP.Tools.Common.HKP (FetchValidationMethod(..))++import Options.Applicative.Builder+ ( argument+ , auto+ , help+ , helpDoc+ , long+ , metavar+ , option+ , showDefault+ , str+ , value+ )+import Options.Applicative.Types (Parser)+import Prettyprinter+ ( hardline+ , list+ , pretty+ )++data LintOutputFormat+ = Pretty+ | JSON+ | YAML+ deriving (Bounded, Enum, Eq, Read, Show)++data LintOptions =+ LintOptions+ { lintOutputFormat :: LintOutputFormat+ }++data FetchOptions =+ FetchOptions+ { keyServer :: String+ , fetchMethod :: FetchMethod+ , fetchValidation :: FetchValidationMethod+ , fetchQuery :: String+ }++data InjectSSHAgentOptions =+ InjectSSHAgentOptions+ { injectSSHAgentFromFD :: Maybe Int+ , injectSSHAgentSocket :: Maybe String+ , injectSSHAgentComment :: Maybe String+ }++data FetchMethod+ = HKP+ | WKD+ deriving (Bounded, Enum, Eq, Read, Show)++lintO :: Parser LintOptions+lintO =+ LintOptions <$>+ option+ auto+ (long "output-format" <> metavar "FORMAT" <> value Pretty <> showDefault <>+ ofHelp)+ where+ ofHelp =+ helpDoc . Just $ pretty "output format" <> hardline <>+ list (map (pretty . show) ofchoices)+ ofchoices = [minBound .. maxBound] :: [LintOutputFormat]++fetchO :: Parser FetchOptions+fetchO =+ FetchOptions <$>+ option+ str+ (long "keyserver" <> metavar "URL" <>+ value "http://pool.sks-keyservers.net:11371" <>+ showDefault <>+ help "HKP server (used only when --method=HKP)") <*>+ option+ auto+ (long "method" <> metavar "METHOD" <> value HKP <> showDefault <> fmHelp) <*>+ option+ auto+ (long "validation-method" <> metavar "METHOD" <>+ value MatchPrimaryKeyFingerprint <>+ showDefault <>+ vmHelp) <*>+ argument str (metavar "QUERY")+ where+ fmHelp =+ helpDoc . Just $ pretty "fetch method" <> hardline <>+ list (map (pretty . show) fmchoices)+ fmchoices = [minBound .. maxBound] :: [FetchMethod]+ vmHelp =+ helpDoc . Just $ pretty "validation method" <> hardline <>+ list (map (pretty . show) vmchoices)+ vmchoices = [minBound .. maxBound] :: [FetchValidationMethod]++injectSSHAgentO :: Parser InjectSSHAgentOptions+injectSSHAgentO =+ InjectSSHAgentOptions <$>+ optional+ (option+ auto+ (long "from-fd" <>+ metavar "FD" <>+ help "read binary gpg --export-secret-keys bytes from this already-open file descriptor")) <*>+ optional+ (option+ str+ (long "ssh-agent-socket" <>+ metavar "PATH" <> help "path to ssh-agent socket (defaults to SSH_AUTH_SOCK)")) <*>+ optional+ (option+ str+ (long "comment" <>+ metavar "TEXT" <> help "comment string stored with the injected SSH identity"))
− HOpenPGP/Tools/Lexer.x
@@ -1,175 +0,0 @@-{-{-# OPTIONS -w #-}-module HOpenPGP.Tools.Lexer-( alexEOF-, alexSetInput-, alexGetInput-, alexError-, alexScan-, ignorePendingBytes-, alexGetStartCode-, runAlex-, Alex(..)-, Token(..)-, AlexReturn(..)-, AlexPosn(..)-) where--import Prelude hiding (lex)-import Numeric (readHex)-import Codec.Encryption.OpenPGP.Types (Fingerprint(..), EightOctetKeyId(..))--}--%wrapper "monad"--$digit = 0-9-$hexdigit = [0-9A-Fa-f]--tokens :-- $white+ ;- a { lex' TokenA }- and { lex' TokenAnd }- any { lex' TokenAny }- every { lex' TokenEvery }- not { lex' TokenNot }- now { lex' TokenNow }- one { lex' TokenOne }- or { lex' TokenOr }- subkey { lex' TokenSubkey }- tag { lex' TokenTag }- of { lex' TokenOf }- \=\= { lex' TokenEq }- \= { lex' TokenEq }- equals { lex' TokenEq }- \< { lex' TokenLt }- \> { lex' TokenGt }- \( { lex' TokenLParen }- \) { lex' TokenRParen }- contains { lex' TokenContains }- pkversion { lex' TokenPKVersion }- sigversion { lex' TokenSigVersion }- [Ss]ig[Tt]ype { lex' TokenSigType }- [Pp][Kk][Aa]lgo { lex' TokenPKAlgo }- [Ss]ig[Pp][Kk][Aa]lgo { lex' TokenSigPKAlgo }- [Hh]ash[Aa]lgo { lex' TokenHashAlgo }- [Rr][Ss][Aa] { lex' TokenRSA }- [Dd][Ss][Aa] { lex' TokenDSA }- [Ee]l[Gg]amal { lex' TokenElgamal }- [Ee][Cc][Dd][Ss][Aa] { lex' TokenECDSA }- [Ee][Cc][Dd][Hh] { lex' TokenECDH }- [Dd][Hh] { lex' TokenDH }- [Bb]inary { lex' TokenBinary }- [Cc]anonical[Tt]ext { lex' TokenCanonicalText }- [Ss]tandalone { lex' TokenStandalone }- [Gg]eneric[Cc]ert { lex' TokenGenericCert }- [Pp]ersona[Cc]ert { lex' TokenPersonaCert }- [Cc]asual[Cc]ert { lex' TokenCasualCert }- [Pp]ositive[Cc]ert { lex' TokenPositiveCert }- [Ss]ubkey[Bb]inding[Ss]ig { lex' TokenSubkeyBindingSig }- [Pp]rimary[Kk]ey[Bb]inding[Ss]ig { lex' TokenPrimaryKeyBindingSig }- [Ss]ignature[Dd]irectly[Oo]n[Aa][Kk]ey { lex' TokenSignatureDirectlyOnAKey }- [Kk]ey[Rr]evocation[Ss]ig { lex' TokenKeyRevocationSig }- [Ss]ubkey[Rr]evocation[Ss]ig { lex' TokenSubkeyRevocationSig }- [Cc]ert[Rr]evocation[Ss]ig { lex' TokenCertRevocationSig }- [Tt]imestamp[Ss]ig { lex' TokenTimestampSig }- [Mm][Dd]5 { lex' TokenMD5 }- [Ss][Hh][Aa]1 { lex' TokenSHA1 }- [Rr][Ii][Pp][Ee][Mm][Dd]160 { lex' TokenRIPEMD160 }- [Ss][Hh][Aa]256 { lex' TokenSHA256 }- [Ss][Hh][Aa]384 { lex' TokenSHA384 }- [Ss][Hh][Aa]512 { lex' TokenSHA512 }- [Ss][Hh][Aa]224 { lex' TokenSHA224 }- [Uu][Ii][Dd]s { lex' TokenUids }- keysize { lex' TokenKeysize }- length { lex' TokenLength }- timestamp { lex' TokenTimestamp }- fingerprint { lex' TokenFingerprint }- keyid { lex' TokenKeyID }- [Ss]ig[Cc]reation[Tt]ime { lex' TokenSigCreationTime }- $hexdigit{9}$hexdigit{9}$hexdigit{9}$hexdigit{9}$hexdigit{4} { lex (TokenFpr . read) }- 0x$hexdigit{9}$hexdigit{9}$hexdigit{9}$hexdigit{9}$hexdigit{4} { lex (TokenFpr . read . drop 2) }- $hexdigit{8}$hexdigit{8} { lex (TokenLongID . Right . read) }- 0x$hexdigit{8}$hexdigit{8} { lex (TokenLongID . Right . read . drop 2) }- $digit+ { lex (TokenInt . fromIntegral . read) }- $hexdigit+ { lex (TokenInt . fromIntegral . fst . head . readHex) }- 0x$hexdigit+ { lex (TokenInt . fromIntegral . fst . head . readHex . drop 2) }- \".*\" { lex (TokenStr . ((zipWith const . drop 1) <*> (drop 2))) }--{-data Token- = TokenTag- | TokenAfter- | TokenAnd- | TokenAny- | TokenBefore- | TokenNot- | TokenNow- | TokenOr- | TokenInt Integer- | TokenEq- | TokenLt- | TokenGt- | TokenLParen- | TokenRParen- | TokenEOF- | TokenPKVersion- | TokenSigVersion- | TokenSigType- | TokenPKAlgo- | TokenSigPKAlgo- | TokenHashAlgo- | TokenRSA- | TokenDSA- | TokenElgamal- | TokenECDSA- | TokenECDH- | TokenDH- | TokenBinary- | TokenCanonicalText- | TokenStandalone- | TokenGenericCert- | TokenPersonaCert- | TokenCasualCert- | TokenPositiveCert- | TokenSubkeyBindingSig- | TokenPrimaryKeyBindingSig- | TokenSignatureDirectlyOnAKey- | TokenKeyRevocationSig- | TokenSubkeyRevocationSig- | TokenCertRevocationSig- | TokenTimestampSig- | TokenMD5- | TokenSHA1- | TokenRIPEMD160- | TokenSHA256- | TokenSHA384- | TokenSHA512- | TokenSHA224- | TokenKeysize- | TokenTimestamp- | TokenFingerprint- | TokenKeyID- | TokenFpr Fingerprint- | TokenLongID (Either String EightOctetKeyId)- | TokenLength- | TokenEvery- | TokenOne- | TokenOf- | TokenContains- | TokenUids- | TokenStr String- | TokenA- | TokenSubkey- | TokenSigCreationTime- deriving (Eq,Show)--alexEOF = return TokenEOF--lex :: (String -> a) -> AlexAction a-lex f = \(_,_,_,s) i -> return (f (take i s))--lex' :: a -> AlexAction a-lex' = lex . const--}
− HOpenPGP/Tools/Parser.y
@@ -1,229 +0,0 @@-{-{-# OPTIONS -w #-}-module HOpenPGP.Tools.Parser( parseTKExp, parsePExp ) where-import Codec.Encryption.OpenPGP.Types--- import Data.Conduit.OpenPGP.Filter (Expr(..), UPredicate(..), UOp(..), OVar(..), OValue(..), SPVar(..), SPValue(..), PKPVar(..), PKPValue(..))-import HOpenPGP.Tools.Common (pkpGetPKVersion, pkpGetPKAlgo, pkpGetKeysize, pkpGetTimestamp, pkpGetFingerprint, pkpGetEOKI, tkUsingPKP, tkGetUIDs, tkGetSubs, anyOrAll, anyReader, oGetTag, oGetLength, spGetSigVersion, spGetSigType, spGetPKAlgo, spGetHashAlgo, spGetSCT, pUsingPKP, pUsingSP, maybeR)--import HOpenPGP.Tools.Lexer--import Control.Applicative (liftA2)-import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)-import Codec.Encryption.OpenPGP.KeyInfo (pubkeySize)-import Control.Error.Util (hush)-import Control.Monad.Loops (allM, anyM)-import Control.Monad.Trans.Reader (ask, reader, Reader, withReader)-import Data.List (isInfixOf)-import qualified Data.Text as T-import Prettyprinter (pretty)--}--%name parseTK Exp-%name parseP CFExp-%tokentype { Token }-%monad { Alex }-%lexer { lexwrap } { TokenEOF }-%error { happyError }-%token- a { TokenA }- and { TokenAnd }- any { TokenAny }- contains { TokenContains }- every { TokenEvery }- not { TokenNot }- now { TokenNow }- of { TokenOf }- one { TokenOne }- or { TokenOr }- subkey { TokenSubkey }- tag { TokenTag }- int { TokenInt $$ }- '=' { TokenEq }- '<' { TokenLt }- '>' { TokenGt }- '(' { TokenLParen }- ')' { TokenRParen }- pkversion { TokenPKVersion }- sigversion { TokenSigVersion }- sigtype { TokenSigType }- pkalgo { TokenPKAlgo }- sigpkalgo { TokenSigPKAlgo }- hashalgo { TokenHashAlgo }- rsa { TokenRSA }- dsa { TokenDSA }- elgamal { TokenElgamal }- ecdsa { TokenECDSA }- ecdh { TokenECDH }- dh { TokenDH }- binary { TokenBinary }- canonicaltext { TokenCanonicalText }- standalone { TokenStandalone }- genericcert { TokenGenericCert }- personacert { TokenPersonaCert }- casualcert { TokenCasualCert }- positivecert { TokenPositiveCert }- subkeybindingsig { TokenSubkeyBindingSig }- primarykeybindingsig { TokenPrimaryKeyBindingSig }- signaturedirectlyonakey { TokenSignatureDirectlyOnAKey }- keyrevocationsig { TokenKeyRevocationSig }- subkeyrevocationsig { TokenSubkeyRevocationSig }- certrevocationsig { TokenCertRevocationSig }- timestampsig { TokenTimestampSig }- md5 { TokenMD5 }- sha1 { TokenSHA1 }- ripemd160 { TokenRIPEMD160 }- sha256 { TokenSHA256 }- sha384 { TokenSHA384 }- sha512 { TokenSHA512 }- sha224 { TokenSHA224 }- keysize { TokenKeysize }- timestamp { TokenTimestamp }- fingerprint { TokenFingerprint }- keyid { TokenKeyID }- sigcreationtime { TokenSigCreationTime }- fpr { TokenFpr $$ }- longid { TokenLongID $$ }- length { TokenLength }- str { TokenStr $$ }- uids { TokenUids }--%%--Exp : any { return True }- | not Exp { fmap not $2 }- | Exp and Exp { liftA2 (&&) $1 $3 }- | Exp or Exp { liftA2 (||) $1 $3 }- | PExp { tkUsingPKP $1 }- | TExp { $1 }--PExp : pkversion PIOp int { $2 (reader pkpGetPKVersion) (return $3) }- | pkalgo PIOp Ppkalgos { $2 (reader pkpGetPKAlgo) (return $3) }- | keysize PIOp int { $2 (reader pkpGetKeysize) (return $3) }- | timestamp PIOp int { $2 (reader pkpGetTimestamp) (return $3) }- | fingerprint PSOp Pfingerprint { $2 (reader (show . pretty . pkpGetFingerprint)) (return $3) }- | keyid PSOp Plongid { $2 (reader pkpGetEOKI) (return $3) }--TExp : every one of uids AATOp str { withReader tkGetUIDs (anyOrAll allM ($5 (return (T.pack $6)))) }- | any one of uids AATOp str { withReader tkGetUIDs (anyOrAll anyM ($5 (return (T.pack $6)))) }- | any of uids AATOp str { withReader tkGetUIDs (anyOrAll anyM ($4 (return (T.pack $5)))) }- | a subkey PExp { withReader tkGetSubs (anyReader $3) }--PIOp : '=' { liftA2 (==) }- | '<' { liftA2 (<) }- | '>' { liftA2 (>) }--PSOp : '=' { liftA2 (==) }- | contains { liftA2 (flip isInfixOf) }--AATOp : '=' { liftA2 (==) }- | contains { liftA2 T.isInfixOf }--Ppkalgos : rsa { fromIntegral (fromFVal RSA) }- | dsa { fromIntegral (fromFVal DSA) }- | elgamal { fromIntegral (fromFVal ElgamalEncryptOnly) }- | ecdsa { fromIntegral (fromFVal ECDSA) }- | ecdh { fromIntegral (fromFVal ECDH) }- | dh { fromIntegral (fromFVal DH) }- | int { fromIntegral $1 }--Pfingerprint : fpr { (show . pretty) $1 }--Plongid : longid { either (const "BROKEN") show $1 }--CFExp : any { return True }- | not CFExp { fmap not $2 }- | CFExp and CFExp { liftA2 (&&) $1 $3 }- | CFExp or CFExp { liftA2 (||) $1 $3 }- | OExp { $1 }- | SPExp { $1 }- | PExp { pUsingPKP (maybeR True $1) }--OExp : tag OIOp int { $2 (reader oGetTag) (return $3) }- | length OIOp int { $2 (reader oGetLength) (return $3) }--OIOp : '=' { liftA2 (==) }- | '<' { liftA2 (<) }- | '>' { liftA2 (>) }--SPExp : sigversion SIOp int { $2 (reader spGetSigVersion) (return (Just $3)) }- | sigtype SIOp Ssigtypes { $2 (reader spGetSigType) (return (Just $3)) }- | sigpkalgo SIOp Spkalgos { $2 (reader spGetPKAlgo) (return (Just $3)) }- | hashalgo SIOp Shashalgos { $2 (reader spGetHashAlgo) (return (Just $3)) }- | sigcreationtime SIOp Stimespec { $2 (reader spGetSCT) (return (Just $3)) }--SIOp : '=' { liftA2 (==) }- | '<' { liftA2 (<) }- | '>' { liftA2 (>) }--Ssigtypes : binary { fromIntegral (fromFVal BinarySig) }- | canonicaltext { fromIntegral (fromFVal CanonicalTextSig) }- | standalone { fromIntegral (fromFVal StandaloneSig) }- | genericcert { fromIntegral (fromFVal GenericCert) }- | personacert { fromIntegral (fromFVal PersonaCert) }- | casualcert { fromIntegral (fromFVal CasualCert) }- | positivecert { fromIntegral (fromFVal PositiveCert) }- | subkeybindingsig { fromIntegral (fromFVal SubkeyBindingSig) }- | primarykeybindingsig { fromIntegral (fromFVal PrimaryKeyBindingSig) }- | signaturedirectlyonakey { fromIntegral (fromFVal SignatureDirectlyOnAKey) }- | keyrevocationsig { fromIntegral (fromFVal KeyRevocationSig) }- | subkeyrevocationsig { fromIntegral (fromFVal SubkeyRevocationSig) }- | certrevocationsig { fromIntegral (fromFVal CertRevocationSig) }- | timestampsig { fromIntegral (fromFVal TimestampSig) }- | int { fromIntegral $1 }--Spkalgos : rsa { fromIntegral (fromFVal RSA) }- | dsa { fromIntegral (fromFVal DSA) }- | elgamal { fromIntegral (fromFVal ElgamalEncryptOnly) }- | ecdsa { fromIntegral (fromFVal ECDSA) }- | ecdh { fromIntegral (fromFVal ECDH) }- | dh { fromIntegral (fromFVal DH) }- | int { fromIntegral $1 }--Shashalgos : md5 { fromIntegral (fromFVal DeprecatedMD5) }- | sha1 { fromIntegral (fromFVal SHA1) }- | ripemd160 { fromIntegral (fromFVal RIPEMD160) }- | sha256 { fromIntegral (fromFVal SHA256) }- | sha384 { fromIntegral (fromFVal SHA384) }- | sha512 { fromIntegral (fromFVal SHA512) }- | sha224 { fromIntegral (fromFVal SHA224) }- | int { fromIntegral $1 }--Stimespec : now { 0 }- | int { fromIntegral $1 }--{-lexwrap :: (Token -> Alex a) -> Alex a-lexwrap cont = do- t <- alexMonadScan'- cont t--alexMonadScan' = do- inp <- alexGetInput- sc <- alexGetStartCode- case alexScan inp sc of- AlexEOF -> alexEOF- AlexError (pos, _, _, _) -> alexError (show pos)- AlexSkip inp' len -> do- alexSetInput inp'- alexMonadScan'- AlexToken inp' len action -> do- alexSetInput inp'- action (ignorePendingBytes inp) len--getPosn :: Alex (Int,Int)-getPosn = do- (AlexPn _ l c,_,_,_) <- alexGetInput- return (l,c)--happyError :: Token -> Alex a-happyError t = do- (l,c) <- getPosn- error (show l ++ ":" ++ show c ++ ": Parse error on Token: " ++ show t ++ "\n")--parseTKExp :: String -> Either String (Reader TKUnknown Bool)-parseTKExp s = runAlex s parseTK--parsePExp :: String -> Either String (Reader Pkt Bool)-parsePExp s = runAlex s parseP-}
− HOpenPGP/Tools/TKUtils.hs
@@ -1,94 +0,0 @@--- TKUtils.hs: hOpenPGP-tools TK-related common functions--- Copyright © 2013-2026 Clint Adams------ vim: softtabstop=4:shiftwidth=4:expandtab------ This program is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as--- published by the Free Software Foundation, either version 3 of the--- License, or (at your option) any later version.------ This program is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with this program. If not, see <http://www.gnu.org/licenses/>.--module HOpenPGP.Tools.TKUtils- ( processTK- ) where--import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)-import Codec.Encryption.OpenPGP.Signatures- ( VerificationError- , verifyAgainstKeys- , verifySigWith- , verifyUnknownTKWith- )-import Codec.Encryption.OpenPGP.Types-import Control.Arrow (second)-import Control.Error.Util (hush)-import Control.Lens ((^.), _1)-import Data.Bifunctor (first)-import Data.List (sortOn)-import Data.Maybe (listToMaybe, mapMaybe)-import Data.Ord (Down(..))-import Data.Time.Clock.POSIX (POSIXTime, posixSecondsToUTCTime)---- should this fail or should verifyUnknownTKWith fail if there are no self-sigs?-processTK :: Maybe POSIXTime -> TKUnknown -> Either String TKUnknown-processTK mpt key =- first show $ verifyUnknownTKWith- (verifySigWith (verifyAgainstKeys [key]))- (fmap posixSecondsToUTCTime mpt) .- stripOlderSigs .- stripOtherSigs $- key- where- stripOtherSigs tk =- tk- { _tkuUIDs = map (second alleged) (_tkuUIDs tk)- , _tkuUAts = map (second alleged) (_tkuUAts tk)- }- stripOlderSigs tk =- tk- { _tkuUIDs = map (second newest) (_tkuUIDs tk)- , _tkuUAts = map (second newest) (_tkuUAts tk)- }- newest = take 1 . sortOn (Down . take 1 . sigcts) -- FIXME: this is terrible- sigcts (SigV4 _ _ _ xs _ _ _) = mapMaybe sigCreationTimeFromSubpacket xs- sigcts (SigV6 _ _ _ _ xs _ _ _) = mapMaybe sigCreationTimeFromSubpacket xs- sigcts _ = []- pkp = key ^. tkuKey . _1- alleged = filter (\x -> assI x || assIFP x)- sigCreationTimeFromSubpacket (SigSubPacket _ (SigCreationTime x)) = Just x- sigCreationTimeFromSubpacket _ = Nothing- sigissuer (SigVOther 2 _) = Nothing- sigissuer SigV3 {} = Nothing- sigissuer (SigV4 _ _ _ ys xs _ _) =- listToMaybe . mapMaybe (getIssuer . _sspPayload) $ (ys ++ xs) -- FIXME: what should this be if there are multiple matches?- sigissuer (SigV6 _ _ _ _ ys xs _ _) =- listToMaybe . mapMaybe (getIssuer . _sspPayload) $ (ys ++ xs) -- FIXME: what should this be if there are multiple matches?- sigissuer (SigVOther _ _) = Nothing- sigissuerfp (SigV4 _ _ _ ys xs _ _) =- listToMaybe . mapMaybe (getIssuerFP . _sspPayload) $ (ys ++ xs) -- FIXME: what should this be if there are multiple matches?- sigissuerfp (SigV6 _ _ _ _ ys xs _ _) =- listToMaybe . mapMaybe (getIssuerFP . _sspPayload) $ (ys ++ xs) -- FIXME: what should this be if there are multiple matches?- sigissuerfp _ = Nothing- eoki- | _keyVersion pkp == V4 = hush . eightOctetKeyID $ pkp- | _keyVersion pkp == DeprecatedV3 &&- elem (_pkalgo pkp) [RSA, DeprecatedRSASignOnly] =- hush . eightOctetKeyID $ pkp- | otherwise = Nothing- fp- | _keyVersion pkp == V4 = Just . fingerprint $ pkp- | otherwise = Nothing- getIssuer (Issuer i) = Just i- getIssuer _ = Nothing- getIssuerFP (IssuerFingerprint IssuerFingerprintV4 i) = Just i- getIssuerFP _ = Nothing- assI x = ((==) <$> sigissuer x <*> eoki) == Just True- assIFP x = ((==) <$> sigissuerfp x <*> fp) == Just True
− HOpenPGP/Tools/WKD.hs
@@ -1,183 +0,0 @@--- WKD.hs: hOpenPGP key tool--- Copyright © 2026 Clint Adams------ vim: softtabstop=4:shiftwidth=4:expandtab------ This program is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as--- published by the Free Software Foundation, either version 3 of the--- License, or (at your option) any later version.------ This program is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with this program. If not, see <http://www.gnu.org/licenses/>.--{-# LANGUAGE OverloadedStrings #-}--module HOpenPGP.Tools.WKD- ( fetchKeys- , parseMailbox- ) where--import qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA-import Codec.Encryption.OpenPGP.ASCIIArmor.Types- ( Armor(Armor)- , ArmorType(ArmorPublicKeyBlock)- )-import Codec.Encryption.OpenPGP.Types (TKUnknown(..))-import Control.Arrow ((&&&))-import Control.Monad.IO.Class (liftIO)-import Control.Monad.Trans.Except (ExceptT(..), throwE)-import qualified Crypto.Hash as CH-import qualified Crypto.Hash.Algorithms as CHA-import Data.Binary (get)-import qualified Data.ByteArray as BA-import qualified Data.ByteString as B-import qualified Data.ByteString.Char8 as BC8-import qualified Data.ByteString.Lazy as BL-import Data.Bits ((.&.), (.|.), shiftL, shiftR)-import Data.Conduit ((.|), runConduitRes)-import qualified Data.Conduit.Binary as CB-import qualified Data.Conduit.List as CL-import Data.Conduit.OpenPGP.Keyring (conduitToTKsDropping)-import Data.Conduit.Serialization.Binary (conduitGet)-import Data.Either (rights)-import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Encoding as TE-import Data.Time.Clock.POSIX (getPOSIXTime)-import Data.Word (Word8)-import HOpenPGP.Tools.HKP (FetchValidationMethod(..))-import HOpenPGP.Tools.TKUtils (processTK)-import Network.HTTP.Client- ( Manager- , Response(..)- , httpLbs- , newManager- , parseUrlThrow- , setQueryString- )-import Network.HTTP.Client.TLS (tlsManagerSettings)-import Network.HTTP.Types.Status (ok200)--fetchKeys ::- FetchValidationMethod- -> Text- -> ExceptT String IO [TKUnknown]-fetchKeys fvm mailbox = do- parsedMailbox <- ExceptT . return $ parseMailbox mailbox- manager <- liftIO $ newManager tlsManagerSettings- response <- fetchWKD manager parsedMailbox- body <-- if responseStatus response == ok200- then return (responseBody response)- else throwE ("HTTP status: " ++ show (responseStatus response))- validateAndFilterKeys fvm parsedMailbox body--parseMailbox :: Text -> Either String (Text, Text)-parseMailbox rawMailbox =- let mailbox = T.strip rawMailbox- parts = T.splitOn "@" mailbox- in case parts of- [localPart, domain]- | T.null localPart -> Left "mailbox local part cannot be empty"- | T.null domain -> Left "mailbox domain cannot be empty"- | T.any (== ' ') mailbox -> Left "mailbox cannot contain spaces"- | otherwise -> Right (localPart, T.toLower domain)- _ -> Left "mailbox must contain exactly one @"--fetchWKD ::- Manager- -> (Text, Text)- -> ExceptT String IO (Response BL.ByteString)-fetchWKD manager (localPart, domain) = do- let localPartLower = T.toLower localPart- hu = BC8.unpack . zbase32 . sha1 . TE.encodeUtf8 $ localPartLower- advancedUrl =- "https://openpgpkey." <>- T.unpack domain <>- "/.well-known/openpgpkey/" <> T.unpack domain <> "/hu/" <> hu- directUrl =- "https://" <> T.unpack domain <> "/.well-known/openpgpkey/hu/" <> hu- mailboxParam = TE.encodeUtf8 localPartLower- withMailbox req = setQueryString [("l", Just mailboxParam)] req- advancedRequest <- liftIO $ parseUrlThrow advancedUrl- advancedResponse <- liftIO $ httpLbs (withMailbox advancedRequest) manager- if responseStatus advancedResponse == ok200- then return advancedResponse- else do- directRequest <- liftIO $ parseUrlThrow directUrl- liftIO $ httpLbs (withMailbox directRequest) manager--validateAndFilterKeys ::- FetchValidationMethod- -> (Text, Text)- -> BL.ByteString- -> ExceptT String IO [TKUnknown]-validateAndFilterKeys fvm mailbox body = do- keys <- decodeWkdResponse body- cpt <- liftIO getPOSIXTime- let processedKeys = rights $ map (uncurry (liftA2 (,)) . (pure &&& processTK (Just cpt))) keys- mailboxFiltered = filter (mailboxMatchesKey mailbox . snd) processedKeys- return $- map fst $- case fvm of- AnySelfSigned -> processedKeys- MatchPrimaryKeyFingerprint -> mailboxFiltered- MatchPrimaryOrAnySubkeyFingerprint -> mailboxFiltered--decodeWkdResponse :: BL.ByteString -> ExceptT String IO [TKUnknown]-decodeWkdResponse body =- if isArmored body- then decodeArmored body- else decodeBinary body--decodeBinary :: BL.ByteString -> ExceptT String IO [TKUnknown]-decodeBinary bytes =- liftIO . runConduitRes $- CB.sourceLbs bytes .| conduitGet get .| conduitToTKsDropping .| CL.consume--decodeArmored :: BL.ByteString -> ExceptT String IO [TKUnknown]-decodeArmored larmors = do- bytestrings <- ExceptT . return $ fmap (mconcat . map armorToBS) (AA.decodeLazy larmors)- liftIO . runConduitRes $- CB.sourceLbs bytestrings .| conduitGet get .| conduitToTKsDropping .| CL.consume- where- armorToBS (Armor ArmorPublicKeyBlock _ bs) = bs- armorToBS _ = mempty--isArmored :: BL.ByteString -> Bool-isArmored =- BC8.isPrefixOf "-----BEGIN PGP PUBLIC KEY BLOCK-----" . BL.toStrict . BL.take 40--mailboxMatchesKey :: (Text, Text) -> TKUnknown -> Bool-mailboxMatchesKey (localPart, domain) tk =- let mailbox = T.toLower (localPart <> "@" <> domain)- bracketedMailbox = "<" <> mailbox <> ">"- in any- (\uid -> let lowered = T.toLower uid in lowered == mailbox || bracketedMailbox `T.isInfixOf` lowered)- (map fst (_tkuUIDs tk))--sha1 :: B.ByteString -> B.ByteString-sha1 bs = BA.convert (CH.hashWith CHA.SHA1 bs :: CH.Digest CHA.SHA1)--zbase32 :: B.ByteString -> B.ByteString-zbase32 = BC8.pack . encodeZBase32 . B.unpack--encodeZBase32 :: [Word8] -> String-encodeZBase32 = go 0 0- where- alphabet = "ybndrfg8ejkmcpqxot1uwisza345h769"- pick i = alphabet !! i- go _ 0 [] = []- go acc bits [] =- [pick (fromIntegral (((acc `shiftL` (5 - bits)) .&. 31) :: Int))]- go acc bits (x:xs)- | bits >= 5 =- pick (fromIntegral (((acc `shiftR` (bits - 5)) .&. 31) :: Int)) :- go acc (bits - 5) (x : xs)- | otherwise = go ((acc `shiftL` 8) .|. fromIntegral x) (bits + 8) xs
hkt.hs view
@@ -76,7 +76,7 @@ import Data.Tuple (swap) import qualified Data.Yaml as Y import GHC.Generics-import HOpenPGP.Tools.Common+import HOpenPGP.Tools.Common.Common ( banner , keyMatchesEightOctetKeyId , keyMatchesFingerprint@@ -84,7 +84,7 @@ , versioner , warranty )-import HOpenPGP.Tools.Parser (parseTKExp)+import HOpenPGP.Tools.Common.Parser (parseTKExp) import System.Directory (getHomeDirectory) import System.Exit (exitFailure) import Options.Applicative.Builder@@ -139,11 +139,23 @@ grabMatchingKeys :: FilePath -> Bool -> Text -> IO [TKUnknown] grabMatchingKeys fp filt srch =- parseFilterPredicateIO srch >>= \parsed ->- case parsed of- Left err -> dieHKT err- Right ufp ->- runConduitRes $ grabMatchingKeysConduit fp filt ufp srch .| CL.consume+ if filt+ then do+ parsed <- parseFilterPredicateIO srch+ case parsed of+ Left err -> dieHKT err+ Right ufp -> runConduitRes $ grabMatchingKeysConduit fp filt ufp srch .| CL.consume+ else+ -- When not using filter syntax, treat TARGET as a fingerprint, key ID or UID substring+ runConduitRes $+ CB.sourceFile fp .| conduitGet get .| conduitToTKsDropping .| CL.filter matchAny .| CL.consume+ where+ matchAny tk =+ either (const False) id $ runExcept $ fmap (keyMatchesFingerprint True tk) efp <|>+ fmap (keyMatchesEightOctetKeyId True tk . Right) eeok <|>+ return (keyMatchesUIDSubString srch tk)+ efp = (except . parseFingerprint) srch+ eeok = (except . parseEightOctetKeyId) srch grabMatchingPublicKeyring :: [TKUnknown] -> IO PublicKeyring grabMatchingPublicKeyring keys =@@ -182,7 +194,10 @@ TKey { publickey = mkey (tk ^. tkuKey . _1) , uids = tk ^. tkuUIDs ^.. traverse . _1- , subkeys = map (mkey . \(PublicSubkeyPkt x, _) -> x) (tk ^. tkuSubs)+ , subkeys = mapMaybe (\t -> case t of+ (PublicSubkeyPkt x, _) -> Just (mkey x)+ (SecretSubkeyPkt x _, _) -> Just (mkey x)+ _ -> Nothing) (tk ^. tkuSubs) } where mkey =
hokey.hs view
@@ -22,1221 +22,89 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE TypeApplications #-} -import Codec.Encryption.OpenPGP.Expirations (getKeyExpirationTimesFromSignature)-import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)-import Codec.Encryption.OpenPGP.KeyInfo (pkalgoAbbrev, pubkeySize)-import Codec.Encryption.OpenPGP.KeySelection (parseFingerprint)-import Codec.Encryption.OpenPGP.Ontology- ( isCT- , isCertRevocationSig- , isKUF- , isPHA- , isPKBindingSig- , isSKBindingSig- )-import Codec.Encryption.OpenPGP.Serialize ()-import Codec.Encryption.OpenPGP.Types-import Control.Applicative (optional)-import Control.Arrow ((***))-import Control.Error.Util (hush)-import Control.Exception (bracket)-import Control.Lens ((&), (^.), _1, _2, mapped, over)-import Control.Monad.Trans.Except (ExceptT(..), runExceptT)-import Control.Monad.Trans.Writer.Lazy (execWriter, tell)-import qualified Crypto.Hash as CH-import qualified Crypto.Hash.Algorithms as CHA-import qualified Crypto.PubKey.RSA as RSA-import qualified Data.Aeson as A-import Data.Binary (get, put)-import Data.Binary.Get (getWord32be, runGet)-import Data.Binary.Put (Put, putByteString, putLazyByteString, putWord32be, putWord8, runPut)-import Data.Bits ((.&.), shiftR, testBit)-import qualified Data.ByteArray as BA-import qualified Data.ByteString as B-import qualified Data.ByteString.Base16 as Base16-import qualified Data.ByteString.Char8 as BC8-import qualified Data.ByteString.Lazy as BL-import Data.Conduit ((.|), runConduitRes)-import qualified Data.Conduit.Binary as CB-import qualified Data.Conduit.List as CL-import Data.Conduit.OpenPGP.Keyring (AuthSecretSubkeyAtTime, authSecretSubkeyPrimaryUID, authSecretSubkeyValue, conduitToAuthSecretSubkeysAt, conduitToSecretTKs, conduitToTKsDropping)-import Data.Conduit.Serialization.Binary (conduitGet, conduitPut)-import Data.Foldable (find)-import Data.List (elemIndex, findIndex, intercalate, nub, sort, sortOn)-import qualified Data.Map as Map-import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)-import Data.Ord (Down(..))-import Data.Semigroup (Semigroup, (<>))-import qualified Data.Set as Set-import Data.Text (Text)-import qualified Data.Text as T-import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime, posixSecondsToUTCTime)-import Data.Time.Format (formatTime)-import qualified Data.Word as Word-import qualified Data.Yaml as Y-import GHC.Generics-import HOpenPGP.Tools.Common (banner, versioner, warranty)-import HOpenPGP.Tools.HKP (FetchValidationMethod(..), rearmorKeys)-import qualified HOpenPGP.Tools.HKP as HKP-import HOpenPGP.Tools.TKUtils (processTK)-import Network.Socket (Family(AF_UNIX), SockAddr(..), Socket, SocketType(Stream), close, connect, defaultProtocol, socket)-import qualified Network.Socket.ByteString as NSB-import System.Environment (lookupEnv)-import System.Exit (exitFailure)-import qualified HOpenPGP.Tools.WKD as WKD--import Options.Applicative.Builder- ( argument- , auto- , command- , footerDoc- , headerDoc- , help- , helpDoc- , info- , long- , metavar- , option- , prefs- , progDesc- , showDefault- , showHelpOnError- , str- , value- )-import Options.Applicative.Extra (customExecParser, helper, hsubparser)-import Options.Applicative.Types (Parser)--import Data.Time.Locale.Compat (defaultTimeLocale)-import System.IO- ( BufferMode(..)- , Handle- , hFlush- , hPutStrLn- , hSetBuffering- , stderr- , stdin- , stdout- )--import Prettyprinter- ( Doc- , (<+>)- , annotate- , colon- , defaultLayoutOptions- , flatAlt- , hardline- , indent- , layoutPretty- , line- , list- , pretty- )-import qualified Prettyprinter.Render.Terminal as PPA--linebreak = flatAlt line mempty--green = annotate (PPA.color PPA.Green)--yellow = annotate (PPA.color PPA.Yellow)--red = annotate (PPA.color PPA.Red)--data KAS =- KAS- { pubkeyalgo :: Colored PubKeyAlgorithm- , pubkeysize :: Colored (Maybe Int)- , stringrep :: String- }- deriving (Generic)--data Color- = Green- | Yellow- | Red- deriving (Eq, Generic, Ord)--data Colored a =- Colored- { color :: Maybe Color- , explanation :: Maybe String- , val :: a- }- deriving (Functor, Generic)--newtype FakeMap a b =- FakeMap- { unFakeMap :: [(a, b)]- }--data KeyReport =- KeyReport- { keyStatus :: String- , keyFingerprint :: Fingerprint- , keyVer :: Colored KeyVersion- , keyCreationTime :: ThirtyTwoBitTimeStamp- , keyAlgorithmAndSize :: KAS- , keyUIDsAndUAts :: FakeMap Text (Colored UIDReport)- , keyBestOf :: Maybe UIDReport- , keySubkeys :: [SubkeyReport]- , keyHasEncryptionCapableSubkey :: Colored Bool- }- deriving (Generic)--data UIDReport =- UIDReport- { uidSelfSigHashAlgorithms :: [Colored HashAlgorithm]- , uidPreferredHashAlgorithms :: [Colored [HashAlgorithm]]- , uidKeyExpirationTimes :: [Colored [ThirtyTwoBitDuration]]- , uidKeyUsageFlags :: [Colored (Set.Set KeyFlag)]- , uidRevocationStatus :: [RevocationStatus]- }- deriving (Generic)--data SubkeyReport =- SubkeyReport- { skFingerprint :: Colored Fingerprint- , skVer :: Colored KeyVersion- , skCreationTime :: ThirtyTwoBitTimeStamp- , skAlgorithmAndSize :: KAS- , skBindingSigHashAlgorithms :: [Colored HashAlgorithm]- , skRevocationSigWeakDigests :: [SubkeyRevocationDigestWarning]- , skUsageFlags :: [Colored (Set.Set KeyFlag)]- , skCrossCerts :: CrossCertReport- }- deriving (Generic)--data SubkeyRevocationDigestWarning =- SubkeyRevocationDigestWarning- { srwHashAlgorithm :: HashAlgorithm- , srwSubkeyFingerprint :: String- , srwSubkeyKeyID :: Maybe String- , srwMessage :: String- }- deriving (Generic)--data CrossCertReport =- CrossCertReport- { ccPresent :: Colored Bool- , ccHashAlgorithms :: [Colored HashAlgorithm]- }- deriving (Generic)--data RevocationStatus =- RevocationStatus- { isRevoked :: Bool- , revocationCode :: String- , revocationReason :: Text- }- deriving (Generic)--instance A.ToJSON KAS--instance A.ToJSON Color--instance (A.ToJSON a) => A.ToJSON (Colored a)--instance A.ToJSON KeyReport--instance A.ToJSON UIDReport--instance A.ToJSON SubkeyReport--instance A.ToJSON SubkeyRevocationDigestWarning--instance A.ToJSON CrossCertReport--instance A.ToJSON b => A.ToJSON (FakeMap Text b) where- toJSON = A.toJSON . Map.fromList . unFakeMap--instance A.ToJSON RevocationStatus--instance Semigroup UIDReport where- (<>) (UIDReport a b c d e) (UIDReport a' b' c' d' e') =- UIDReport (a <> a') (b <> b') (c <> c') (d <> d') (e <> e')--instance Monoid UIDReport where- mempty = UIDReport [] [] [] [] []- mappend = (<>)--checkKey :: Maybe POSIXTime -> TKUnknown -> KeyReport-checkKey mpt key =- (\x ->- x- { keyBestOf = populateBestOf x- , keyHasEncryptionCapableSubkey =- hasEncryptionCapableSubkey (concatMap skUsageFlags (keySubkeys x))- })- KeyReport- { keyStatus = either id (const "good") processedTK- , keyFingerprint = key ^. tkuKey . _1 & fingerprint- , keyVer = key ^. tkuKey . _1 & _keyVersion & colorizeKV- , keyCreationTime = key ^. tkuKey . _1 & _timestamp- , keyAlgorithmAndSize = kasIt (key ^. tkuKey . _1)- , keyUIDsAndUAts =- FakeMap- (map (\(x, y) -> (x, uidr (Just x) y)) (processedOrOrig ^. tkuUIDs) ++- map (uatspsToText *** uidr Nothing) (processedOrOrig ^. tkuUAts))- , keyBestOf = Nothing- , keySubkeys =- map (checkSK (key ^. tkuKey . _1 & fingerprint)) (key ^. tkuSubs)- , keyHasEncryptionCapableSubkey = Colored Nothing Nothing False- }- where- processedOrOrig = either (const key) id processedTK- processedTK = processTK mpt key- kasIt :: SomePKPayload -> KAS- kasIt pkp = kasIt' (_pkalgo pkp) (_pubkey pkp & pubkeySize)- kasIt' :: PubKeyAlgorithm -> Either String Int -> KAS- kasIt' pka epks =- KAS- { pubkeyalgo = colorizePKA pka- , pubkeysize = colorizePKS pka epks- , stringrep = (either (const "unknown") show epks) ++ (pkalgoAbbrev pka)- }- colorizeKV kv =- uncurry- Colored- (if kv == V4- then (Just Green, Nothing)- else (Just Red, Just "not a V4 key"))- kv- colorizePKA pka- | pka `elem` [RSA, EdDSA, ECDH] = Colored (Just Green) Nothing pka- | otherwise =- Colored- (Just Yellow)- (Just "public key algorithm neither RSA nor EdDSA")- pka- colorizePKS pka epks = uncurry Colored (colorizePKS' pka epks) (hush epks)- colorizePKS' pka (Right pks)- | pka `elem` [EdDSA, ECDH] && pks >= 256 = (Just Green, Nothing)- | pka `elem` [EdDSA, ECDH] =- (Just Yellow, Just "Public key size under 256 bits")- | pka == RSA && pks >= 3072 = (Just Green, Nothing)- | pka == RSA && pks >= 2048 =- (Just Yellow, Just "Public key size between 2048 and 3072 bits")- | pka == RSA = (Just Red, Just "Public key size under 2048 bits")- | otherwise = (Nothing, Nothing)- colorizePKS' _ (Left _) =- (Just Red, Just "public key algorithm not understood")- colorizePHAs x =- uncurry- Colored- (if preferredWeakHash x- then (Just Red, Just "weak hash with higher preference")- else (Just Green, Nothing))- x- fSHA2Family = fi (`elem` [SHA512, SHA384, SHA256, SHA224])- firstStrongSHA2 xs = fSHA2Family xs- preferredWeakHash xs =- any- (\ha -> fromMaybe maxBound (elemIndex ha xs) < firstStrongSHA2 xs)- knownWeakHashAlgorithms- fi x y = fromMaybe maxBound (findIndex x y)- colorizeKETs ct ts kes- | null kes = Colored (Just Red) (Just "no expiration set") kes- | any (\ke -> realToFrac ts + realToFrac ke < ct) kes =- Colored (Just Red) (Just "expiration passed") kes- | any (\ke -> realToFrac ts + realToFrac ke > ct + (5 * 31557600)) kes =- Colored (Just Yellow) (Just "expiration too far in future") kes- | otherwise = Colored (Just Green) Nothing kes- eoki pkp- | _keyVersion pkp == V4 = hush . eightOctetKeyID $ pkp- | _keyVersion pkp == DeprecatedV3 &&- elem (_pkalgo pkp) [RSA, DeprecatedRSASignOnly] =- hush . eightOctetKeyID $ pkp- | otherwise = Nothing- phas (SigV4 _ _ _ xs _ _ _) =- colorizePHAs .- concatMap (\(SigSubPacket _ (PreferredHashAlgorithms x)) -> x) $- filter isPHA xs- phas _ = Colored Nothing Nothing []- has = map (colorizeHA . hashAlgo) . alleged- colorizeHA ha =- uncurry- Colored- (if isKnownWeakHashAlgorithm ha- then (Just Red, Just "weak hash algorithm")- else (Nothing, Nothing))- ha- sigcts (SigV4 _ _ _ xs _ _ _) =- map (\(SigSubPacket _ (SigCreationTime x)) -> x) $ filter isCT xs- alleged =- filter- (\x -> ((==) <$> sigissuer x <*> eoki (key ^. tkuKey . _1)) == Just True)- uatspsToText = T.pack . uatspsToString- uatspsToString us =- "<uat:[" ++ intercalate "," (map uaspToString us) ++ "]>"- uaspToString (ImageAttribute hdr d) =- hdrToString hdr ++ ':' : show (BL.length d) ++ ':' :- BC8.unpack (Base16.encode (BA.convert (CH.hashlazy @CHA.SHA3_512 d)))- uaspToString (OtherUASub t d) =- "other-" ++ show t ++ ':' : show (BL.length d) ++ ':' :- BC8.unpack (Base16.encode (BA.convert (CH.hashlazy @CHA.SHA3_512 d)))- hdrToString (ImageHV1 JPEG) = "jpeg"- hdrToString (ImageHV1 fmt) = "image-" ++ show (fromFVal fmt)- uidr Nothing sps =- Colored- Nothing- Nothing- (UIDReport- (has sps)- (map phas sps)- (map- (colorizeKETs- (fromMaybe 0 mpt)- (unThirtyTwoBitTimeStamp (_timestamp (key ^. tkuKey . _1))) .- getKeyExpirationTimesFromSignature)- sps -- should that be 0?- )- (kufs False sps)- (findRevocationReason sps))- uidr (Just u) sps =- colorizeUID- u- (UIDReport- (has sps)- (map phas sps)- (map- (colorizeKETs- (fromMaybe 0 mpt)- (unThirtyTwoBitTimeStamp (_timestamp (key ^. tkuKey . _1))) .- getKeyExpirationTimesFromSignature)- sps -- should that be 0?- )- (kufs False sps)- (findRevocationReason sps))- populateBestOf krep =- Just- (UIDReport <$> best . uidSelfSigHashAlgorithms <*> best .- uidPreferredHashAlgorithms <*>- best .- uidKeyExpirationTimes <*>- best .- uidKeyUsageFlags <*>- pure [] $- mconcat (justTheUIDRs krep))- justTheUIDRs = map (decolorize . snd) . unFakeMap . keyUIDsAndUAts- best = take 1 . sortOn color- decolorize (Colored _ _ x) = x- colorizeUID u- | '(' `elem` T.unpack u =- Colored (Just Yellow) (Just "parenthesis in uid") -- FIXME: be more discerning- | '<' `notElem` T.unpack u =- Colored (Just Yellow) (Just "no left angle bracket in uid") -- FIXME: be more discerning- | otherwise = Colored Nothing Nothing- findRevocationReason = concatMap grabReasons . filter isCertRevocationSig- grabReasons (SigV4 CertRevocationSig _ _ has _ _ _) =- mapMaybe (grabReasons' . _sspPayload) has- grabReasons _ = []- grabReasons' (ReasonForRevocation a b) =- Just (RevocationStatus True (show a) b)- grabReasons' _ = Nothing- kufs s =- map- (colorizeKUFs s . (\(SigSubPacket _ (KeyFlags x)) -> x) .- fromMaybe undefined .- find isKUF .- hasheds) .- newestWith (any isKUF . hasheds)- colorizeKUFs False x =- uncurry- Colored- (if (Set.member EncryptStorageKey x ||- Set.member EncryptCommunicationsKey x) &&- (Set.member SignDataKey x || Set.member CertifyKeysKey x)- then (Just Yellow, Just "both signing & encryption")- else (Just Green, Nothing))- x- colorizeKUFs True x =- uncurry- Colored- (if Set.member CertifyKeysKey x- then (Just Red, Just "certification-capable subkey")- else (if (Set.member EncryptStorageKey x ||- Set.member EncryptCommunicationsKey x) &&- Set.member SignDataKey x- then (Just Yellow, Just "both signing & encryption")- else (Just Green, Nothing)))- x- newestWith p = take 1 . sortOn (Down . take 1 . sigcts) . filter p -- FIXME: this is terrible- hasheds (SigV4 _ _ _ xs _ _ _) = xs- hasheds _ = []- checkSK ::- Fingerprint -> (Pkt, [SignaturePayload]) -> SubkeyReport- checkSK pf (PublicSubkeyPkt pkp, sigs) = checkSK' pf pkp sigs- checkSK pf (SecretSubkeyPkt pkp _, sigs) = checkSK' pf pkp sigs- checkSK' pf pkp sigs =- (\x -> x {skCrossCerts = ccr (map decolorize (skUsageFlags x)) sigs})- SubkeyReport- { skFingerprint = colorizeF pf (fingerprint pkp)- , skVer = colorizeKV (_keyVersion pkp)- , skCreationTime = _timestamp pkp- , skAlgorithmAndSize = kasIt pkp- , skBindingSigHashAlgorithms = has (filter isSKBindingSig sigs)- , skRevocationSigWeakDigests =- subkeyRevocationSigWeakDigests pkp sigs- , skUsageFlags = kufs True (filter isSKBindingSig sigs)- , skCrossCerts = CrossCertReport (Colored Nothing Nothing False) []- }- hasEncryptionCapableSubkey skrs =- if any- ((\x ->- Set.member EncryptStorageKey x ||- Set.member EncryptCommunicationsKey x) .- decolorize)- skrs- then Colored (Just Green) Nothing True- else Colored- (Just Red)- (Just "no encryption-capable subkey present")- False- embeddedSigs =- filter isPKBindingSig . concatMap getEmbeds . filter isSKBindingSig- getEmbeds (SigV4 _ _ _ xs ys _ _) = concatMap getEmbed (xs ++ ys)- getEmbeds _ = []- getEmbed (SigSubPacket _ (EmbeddedSignature sp)) = [sp]- getEmbed _ = []- ccr kufs sigs =- CrossCertReport (colorES kufs sigs) (map (colorizeHA . hashAlgo) sigs)- colorES kufs sigs =- case ( null (embeddedSigs sigs)- , any (Set.member SignDataKey) kufs- , any (Set.member AuthKey) kufs) of- (True, True, True) ->- Colored- (Just Red)- (Just "signing- and auth-capable subkey without cross-cert")- False- (True, True, False) ->- Colored- (Just Red)- (Just "signing-capable subkey without cross-cert")- False- (True, False, True) ->- Colored- (Just Yellow)- (Just "auth-capable subkey without cross-cert")- False- (False, True, True) -> Colored (Just Green) Nothing True- (False, True, False) -> Colored (Just Green) Nothing True- (False, False, True) -> Colored (Just Green) Nothing True- (False, False, False) -> Colored Nothing Nothing True- (True, _, _) -> Colored Nothing Nothing False- colorizeF pf fp =- uncurry- Colored- (if pf == fp- then (Just Red, Just "subkey has same fingerprint as primary key")- else (Just Green, Nothing))- fp- subkeyRevocationSigWeakDigests pkp =- mapMaybe (mkSubkeyRevocationSigWeakDigestWarning pkp) .- filter isSubkeyRevocationSignature- mkSubkeyRevocationSigWeakDigestWarning pkp sig =- let ha = hashAlgo sig- in if isKnownWeakHashAlgorithm ha- then- Just- SubkeyRevocationDigestWarning- { srwHashAlgorithm = ha- , srwSubkeyFingerprint = renderFingerprint (fingerprint pkp)- , srwSubkeyKeyID = fmap renderKeyID (hush (eightOctetKeyID pkp))- , srwMessage =- "subkey revocation signature uses known-weak digest algorithm"- }- else Nothing--prettyKeyReport :: POSIXTime -> TKUnknown -> Doc PPA.AnsiStyle-prettyKeyReport cpt key = do- let keyReport = checkKey (Just cpt) key- execWriter $- tell- (linebreak <> pretty "Key has potential validity" <> colon <+>- pretty (keyStatus keyReport) <>- linebreak <>- pretty "Key has fingerprint" <>- colon <+>- pretty (SpacedFingerprint (keyFingerprint keyReport)) <>- linebreak <>- pretty "Checking to see if key is OpenPGPv4" <>- colon <+>- coloredToColor (pretty . show) (keyVer keyReport) <>- linebreak <>- (\kas ->- pretty "Checking the strength of your primary asymmetric key" <> colon <+>- coloredToColor pretty (pubkeyalgo kas) <+>- coloredToColor (maybe (pretty "unknown") pretty) (pubkeysize kas))- (keyAlgorithmAndSize keyReport) <>- linebreak <>- pretty "Checking user-ID- and user-attribute-related items" <>- colon <>- mconcat- (map- (uidtrip (keyCreationTime keyReport) . gottabeabetterway)- (unFakeMap (keyUIDsAndUAts keyReport))) <>- linebreak <>- pretty "Checking subkeys" <>- colon <>- linebreak <>- indent- 2- (pretty "one of the subkeys is encryption-capable" <> colon <+>- coloredToColor pretty (keyHasEncryptionCapableSubkey keyReport)) <>- mconcat (map subkeyrep (keySubkeys keyReport)) <>- linebreak)- where- coloredToColor f (Colored (Just Green) _ x) = green (f x)- coloredToColor f (Colored (Just Yellow) _ x) = yellow (f x)- coloredToColor f (Colored (Just Red) _ x) = red (f x)- coloredToColor f (Colored Nothing _ x) = f x- uidtrip ts (u, ur)- | null (uidRevocationStatus ur) =- linebreak <> indent 2 (coloredToColor pretty (fmap T.unpack u)) <> colon <>- linebreak <>- indent- 4- (pretty "Self-sig hash algorithms" <> colon <+>- (list . map (coloredToColor pretty) . uidSelfSigHashAlgorithms) ur) <>- linebreak <>- indent- 4- (pretty "Preferred hash algorithms" <> colon <+>- mconcat (map (coloredToColor pretty) (uidPreferredHashAlgorithms ur))) <>- linebreak <>- indent- 4- (pretty "Key expiration times" <> colon <+>- mconcat- (map- (coloredToColor list . fmap (map (pretty . keyExp ts)))- (uidKeyExpirationTimes ur))) <>- linebreak <>- indent- 4- (pretty "Key usage flags" <> colon <+>- (list . map (coloredToColor (pretty . Set.toList)))- (uidKeyUsageFlags ur))- | otherwise =- linebreak <> indent 2 (coloredToColor pretty (fmap T.unpack u)) <> colon <+>- pretty "[revoked]" <>- linebreak <>- indent- 4- (pretty "Revocation code" <> colon <+>- list (map (pretty . revocationCode) (uidRevocationStatus ur))) <>- linebreak <>- indent- 4- (pretty "Revocation reason" <> colon <+>- list- (map- (pretty . T.unpack . revocationReason)- (uidRevocationStatus ur)))- keyExp ts ke =- (show . pretty) ke ++ " = " ++- formatTime- defaultTimeLocale- "%c"- (posixSecondsToUTCTime (realToFrac ts + realToFrac ke))- gottabeabetterway (a, Colored x y z) = (Colored x y a, z)- subkeyrep skr =- linebreak <>- indent- 2- (pretty "fpr" <> colon <+>- coloredToColor pretty (fmap SpacedFingerprint (skFingerprint skr))) <>- linebreak <>- indent 4 (pretty "version" <> colon <+> coloredToColor pretty (skVer skr)) <>- linebreak <>- indent 4 (pretty "timestamp" <> colon <+> pretty (skCreationTime skr)) <>- linebreak <>- indent- 4- ((\kas ->- pretty "algo/size" <> colon <+>- coloredToColor pretty (pubkeyalgo kas) <+>- coloredToColor (maybe (pretty "unknown") pretty) (pubkeysize kas))- (skAlgorithmAndSize skr)) <>- linebreak <>- indent- 4- (pretty "binding sig hash algorithms" <> colon <+>- (list . map (coloredToColor pretty) . skBindingSigHashAlgorithms) skr) <>- linebreak <>- indent- 4- (pretty "weak subkey revocation digests" <> colon <+>- if null (skRevocationSigWeakDigests skr)- then pretty "[]"- else- list- (map- (\w ->- red- (pretty (srwHashAlgorithm w) <> colon <+>- pretty (srwSubkeyFingerprint w) <> colon <+>- maybe (pretty "<no-key-id>") pretty (srwSubkeyKeyID w)))- (skRevocationSigWeakDigests skr))) <>- linebreak <>- indent- 4- (pretty "usage flags" <> colon <+>- (list . map (coloredToColor (pretty . Set.toList))) (skUsageFlags skr)) <>- linebreak <>- indent- 4- (pretty "embedded cross-cert" <> colon <+>- (coloredToColor pretty . ccPresent . skCrossCerts) skr) <>- linebreak <>- indent- 4- (pretty "cross-cert hash algorithms" <> colon <+>- (list . map (coloredToColor pretty) . ccHashAlgorithms . skCrossCerts)- skr)--jsonReport :: POSIXTime -> TKUnknown -> BL.ByteString-jsonReport ps = A.encode . checkKey (Just ps)--yamlReport :: POSIXTime -> TKUnknown -> B.ByteString-yamlReport ps = Y.encode . (: []) . checkKey (Just ps)--data LintOutputFormat- = Pretty- | JSON- | YAML- deriving (Bounded, Enum, Eq, Read, Show)--data LintOptions =- LintOptions- { lintOutputFormat :: LintOutputFormat- }--data FetchOptions =- FetchOptions- { keyServer :: String- , fetchMethod :: FetchMethod- , fetchValidation :: FetchValidationMethod- , fetchQuery :: String- }--data InjectSSHAgentOptions =- InjectSSHAgentOptions- { injectSSHAgentFromFD :: Maybe Int- , injectSSHAgentSocket :: Maybe String- , injectSSHAgentComment :: Maybe String- }--data FetchMethod- = HKP- | WKD- deriving (Bounded, Enum, Eq, Read, Show)--data Command- = CmdLint LintOptions- | CmdCanonicalize- | CmdFetch FetchOptions- | CmdInjectSSHAgent InjectSSHAgentOptions--data InjectableAuthSubkey =- InjectableAuthSubkey- { injectableAuthSubkeyPKP :: SomePKPayload- , injectableAuthSubkeySKA :: SKAddendum- , injectableAuthSubkeyPrimaryUID :: Maybe Text- }--lintO :: Parser LintOptions-lintO =- LintOptions <$>- option- auto- (long "output-format" <> metavar "FORMAT" <> value Pretty <> showDefault <>- ofHelp)- where- ofHelp =- helpDoc . Just $ pretty "output format" <> hardline <>- list (map (pretty . show) ofchoices)- ofchoices = [minBound .. maxBound] :: [LintOutputFormat]--fetchO :: Parser FetchOptions-fetchO =- FetchOptions <$>- option- str- (long "keyserver" <> metavar "URL" <>- value "http://pool.sks-keyservers.net:11371" <>- showDefault <>- help "HKP server (used only when --method=HKP)") <*>- option- auto- (long "method" <> metavar "METHOD" <> value HKP <> showDefault <> fmHelp) <*>- option- auto- (long "validation-method" <> metavar "METHOD" <>- value MatchPrimaryKeyFingerprint <>- showDefault <>- vmHelp) <*>- argument str (metavar "QUERY")- where- fmHelp =- helpDoc . Just $ pretty "fetch method" <> hardline <>- list (map (pretty . show) fmchoices)- fmchoices = [minBound .. maxBound] :: [FetchMethod]- vmHelp =- helpDoc . Just $ pretty "validation method" <> hardline <>- list (map (pretty . show) vmchoices)- vmchoices = [minBound .. maxBound] :: [FetchValidationMethod]--injectSSHAgentO :: Parser InjectSSHAgentOptions-injectSSHAgentO =- InjectSSHAgentOptions <$>- optional- (option- auto- (long "from-fd" <>- metavar "FD" <>- help "read binary gpg --export-secret-keys bytes from this already-open file descriptor")) <*>- optional- (option- str- (long "ssh-agent-socket" <>- metavar "PATH" <> help "path to ssh-agent socket (defaults to SSH_AUTH_SOCK)")) <*>- optional- (option- str- (long "comment" <>- metavar "TEXT" <> help "comment string stored with the injected SSH identity"))--dispatch :: Command -> IO ()-dispatch (CmdFetch o) = banner' stderr >> hFlush stderr >> doFetch o-dispatch (CmdLint o) = banner' stderr >> hFlush stderr >> doLint o-dispatch CmdCanonicalize = banner' stderr >> hFlush stderr >> doCanonicalize-dispatch (CmdInjectSSHAgent o) =- banner' stderr >> hFlush stderr >> doInjectSSHAgent o--main :: IO ()-main = do- hSetBuffering stderr LineBuffering- customExecParser- (prefs showHelpOnError)- (info- (helper <*> versioner "hokey" <*> cmd)- (headerDoc (Just (banner "hokey")) <>- progDesc "hOpenPGP Key utility" <>- footerDoc (Just (warranty "hokey")))) >>=- dispatch--cmd :: Parser Command-cmd =- hsubparser- (command- "canonicalize"- (info- (pure CmdCanonicalize)- (progDesc "arrange key components in a canonical ordering")) <>- command- "fetch"- (info- (CmdFetch <$> fetchO)- (progDesc "fetch key(s) via HKP or WKD")) <>- command- "inject-ssh-agent"- (info- (CmdInjectSSHAgent <$> injectSSHAgentO)- (progDesc "Read exported secret key bytes, pick an auth-capable subkey, and add it to ssh-agent")) <>- command- "lint"- (info (CmdLint <$> lintO) (progDesc "check key(s) for 'best practices'")))--doLint :: LintOptions -> IO ()-doLint o = do- cpt <- getPOSIXTime- keys <-- runConduitRes $ CB.sourceHandle stdin .| conduitGet get .|- conduitToTKsDropping .|- CL.consume- output (lintOutputFormat o) cpt keys- where- output Pretty cpt = mapM_ (PPA.putDoc . prettyKeyReport cpt)- output JSON cpt =- mapM_ (BL.putStr . flip BL.append (BL.singleton 0x0a) . jsonReport cpt)- output YAML cpt = mapM_ (B.putStr . yamlReport cpt)--doCanonicalize :: IO ()-doCanonicalize =- runConduitRes $ CB.sourceHandle stdin .| conduitGet get .|- conduitToTKsDropping .|- CL.map canonicalize .|- CL.map put .|- conduitPut .|- CB.sinkHandle stdout- where- canonicalize tk =- tk- { _tkuRevs = sort (_tkuRevs tk)- , _tkuUIDs = indepthsort (_tkuUIDs tk)- , _tkuUAts = indepthsort (_tkuUAts tk)- , _tkuSubs = indepthsort (_tkuSubs tk)- }- indepthsort :: (Ord a, Ord b) => [(a, [b])] -> [(a, [b])]- indepthsort = nub . sort . over (mapped . _2) sort--doFetch :: FetchOptions -> IO ()-doFetch o = do- ekeys <-- runExceptT $- case fetchMethod o of- HKP -> do- fp <- ExceptT . return . parseFingerprint . T.pack $ fetchQuery o- HKP.fetchKeys (keyServer o) (fetchValidation o) fp- WKD -> WKD.fetchKeys (fetchValidation o) (T.pack (fetchQuery o))- case ekeys of- Left e -> hPutStrLn stderr $ "error fetching keys: " ++ e- Right ks -> B.putStr $ rearmorKeys ks--doInjectSSHAgent :: InjectSSHAgentOptions -> IO ()-doInjectSSHAgent opts = do- socketPath <- resolveSSHAgentSocketPath (injectSSHAgentSocket opts)- input <- readInjectedSecretKeyMaterial opts- cpt <- getPOSIXTime- authCandidates <-- runConduitRes $- CL.sourceList (BL.toChunks input) .| conduitGet get .| conduitToSecretTKs .|- conduitToAuthSecretSubkeysAt (posixSecondsToUTCTime cpt) .|- CL.consume- injectableCandidates <-- if null authCandidates- then inferInjectableAuthSubkeys cpt input- else pure (map candidateFromAuthSecretSubkey authCandidates)- whenEmpty injectableCandidates "inject-ssh-agent: no authentication-capable secret subkey found"- selectedRequests <-- selectInjectableAuthSubkeys- (injectSSHAgentComment opts)- injectableCandidates- mapM_- (\(selected, request) -> do- sendAddIdentityToSSHAgent socketPath request- hPutStrLn stderr $- "inject-ssh-agent: added authentication subkey " ++- renderFingerprint (fingerprint (injectableAuthSubkeyPKP selected)) ++- " to ssh-agent")- selectedRequests--candidateFromAuthSecretSubkey :: AuthSecretSubkeyAtTime -> InjectableAuthSubkey-candidateFromAuthSecretSubkey authSubkey =- InjectableAuthSubkey- { injectableAuthSubkeyPKP = authSecretSubkeyPKP authSubkey- , injectableAuthSubkeySKA = authSecretSubkeySKA authSubkey- , injectableAuthSubkeyPrimaryUID = authSecretSubkeyPrimaryUID authSubkey- }--inferInjectableAuthSubkeys ::- POSIXTime -> BL.ByteString -> IO [InjectableAuthSubkey]-inferInjectableAuthSubkeys _cpt input = do- tks <-- runConduitRes $- CL.sourceList (BL.toChunks input) .| conduitGet get .| conduitToSecretTKs .|- CL.consume- pure (concatMap inferFromTK tks)- where- inferFromTK tk =- let mPrimaryUID = fst <$> listToMaybe (_tkUIDs tk)- in mapMaybe (inferFromSubkey mPrimaryUID) (_tkSubs tk)- inferFromSubkey :: Maybe Text -> (KeyPkt k, [SignaturePayload]) -> Maybe InjectableAuthSubkey- inferFromSubkey mPrimaryUID (KeyPktSecretSubkey pkp ska, sigs)- | hasAuthCapability sigs =- Just- InjectableAuthSubkey- { injectableAuthSubkeyPKP = pkp- , injectableAuthSubkeySKA = ska- , injectableAuthSubkeyPrimaryUID = mPrimaryUID- }- inferFromSubkey _ _ = Nothing- hasAuthCapability sigs =- any- (Set.member AuthKey)- (mapMaybe signatureKeyFlags (newestWithUsageFlags (filter isSKBindingSig sigs)))- newestWithUsageFlags =- take 1 . sortOn (Down . take 1 . sigCreationTimes) . filter (any isKUF . signatureHashedSubpackets)- sigCreationTimes = mapMaybe sigCreationTimeFromSubpacket . signatureHashedSubpackets- sigCreationTimeFromSubpacket (SigSubPacket _ (SigCreationTime ct)) = Just ct- sigCreationTimeFromSubpacket _ = Nothing- signatureHashedSubpackets (SigV4 _ _ _ hasheds _ _ _) = hasheds- signatureHashedSubpackets (SigV6 _ _ _ _ hasheds _ _ _) = hasheds- signatureHashedSubpackets _ = []- signatureKeyFlags sig = do- sp <- find isKUF (signatureHashedSubpackets sig)- case sp of- SigSubPacket _ (KeyFlags flags) -> Just flags- _ -> Nothing--resolveSSHAgentSocketPath :: Maybe String -> IO String-resolveSSHAgentSocketPath (Just path) = pure path-resolveSSHAgentSocketPath Nothing = do- envPath <- lookupEnv "SSH_AUTH_SOCK"- case envPath of- Just path -> pure path- Nothing ->- failInject- "inject-ssh-agent: SSH_AUTH_SOCK is not set; use --ssh-agent-socket"--readInjectedSecretKeyMaterial :: InjectSSHAgentOptions -> IO BL.ByteString-readInjectedSecretKeyMaterial opts = do- chunks <-- case injectSSHAgentFromFD opts of- Nothing -> runConduitRes $ CB.sourceHandle stdin .| CL.consume- Just fd- | fd < 0 ->- failInject "inject-ssh-agent: --from-fd must be a non-negative integer"- | otherwise ->- runConduitRes $ CB.sourceFile ("/dev/fd/" ++ show fd) .| CL.consume- let input = BL.fromChunks chunks- if BL.null input- then- failInject- "inject-ssh-agent: no secret key bytes were provided on the selected input stream"- else pure input--selectInjectableAuthSubkeys ::- Maybe String -> [InjectableAuthSubkey] -> IO [(InjectableAuthSubkey, BL.ByteString)]-selectInjectableAuthSubkeys mComment candidates- | null selectedRequests =- failInject- ("inject-ssh-agent: auth-capable subkeys were found, but none are supported for ssh-agent injection: " ++- intercalate "; " (reverse errs))- | otherwise = pure (reverse selectedRequests)- where- (errs, selectedRequests) = foldl' pick ([], []) candidates- pick (accErrs, accSelected) candidate =- case- sshAddIdentityRequest- (fromMaybe (defaultSSHComment candidate) mComment)- (injectableAuthSubkeyPKP candidate)- (injectableAuthSubkeySKA candidate) of- Left err -> (err : accErrs, accSelected)- Right request -> (accErrs, (candidate, request) : accSelected)- defaultSSHComment candidate =- case injectableAuthSubkeyPrimaryUID candidate of- Just uid -> T.unpack uid- Nothing -> "openpgp:" ++ BC8.unpack (Base16.encode (BL.toStrict (unFingerprint (fingerprint (injectableAuthSubkeyPKP candidate)))))--sshAddIdentityRequest ::- String -> SomePKPayload -> SKAddendum -> Either String BL.ByteString-sshAddIdentityRequest comment subkeyPKP subkeySKA =- case subkeySKA of- SUUnencrypted (RSAPrivateKey (RSA_PrivateKey rsaPrivateKey)) _ ->- Right $ frameSSHAgentRequest (rsaAddIdentityPayload (BC8.pack comment) rsaPrivateKey)- SUUnencrypted (EdDSAPrivateKey Ed25519 secretSeed) _ ->- frameSSHAgentRequest <$>- ed25519AddIdentityPayload (BC8.pack comment) subkeyPKP secretSeed- SUUnencrypted (UnknownSKey rawSecret) _- | isEd25519PKA (_pkalgo subkeyPKP) ->- frameSSHAgentRequest <$>- ed25519AddIdentityPayload- (BC8.pack comment)- subkeyPKP- (BL.toStrict rawSecret)- SUUnencrypted (EdDSAPrivateKey Ed448 _) _ ->- Left- ("subkey " ++ renderFingerprint (fingerprint subkeyPKP) ++- " uses Ed448, which is not supported by ssh-agent add-identity")- SUUnencrypted _ _ ->- Left- ("subkey " ++ renderFingerprint (fingerprint subkeyPKP) ++- " uses an unsupported key algorithm for ssh-agent injection")- _ ->- Left- ("subkey " ++ renderFingerprint (fingerprint subkeyPKP) ++- " is encrypted; decrypt it before injection")--authSecretSubkeyPKP :: AuthSecretSubkeyAtTime -> SomePKPayload-authSecretSubkeyPKP = keyPktPKPayload . authSecretSubkeyValue--authSecretSubkeySKA :: AuthSecretSubkeyAtTime -> SKAddendum-authSecretSubkeySKA = secretKeyPktSKAddendum . authSecretSubkeyValue--rsaAddIdentityPayload :: B.ByteString -> RSA.PrivateKey -> BL.ByteString-rsaAddIdentityPayload comment privateKey =- runPut $ do- putWord8 17- putSSHString (BC8.pack "ssh-rsa")- putSSHMpint (RSA.public_n (RSA.private_pub privateKey))- putSSHMpint (RSA.public_e (RSA.private_pub privateKey))- putSSHMpint (RSA.private_d privateKey)- putSSHMpint (RSA.private_qinv privateKey)- putSSHMpint (RSA.private_p privateKey)- putSSHMpint (RSA.private_q privateKey)- putSSHString comment--ed25519AddIdentityPayload ::- B.ByteString -> SomePKPayload -> B.ByteString -> Either String BL.ByteString-ed25519AddIdentityPayload comment pkp rawSecret = do- publicKey <- ed25519PublicPoint pkp- secretSeed <- normalizeEd25519Secret rawSecret- pure $- runPut $ do- putWord8 17- putSSHString (BC8.pack "ssh-ed25519")- putSSHString publicKey- putSSHString (secretSeed <> publicKey)- putSSHString comment--ed25519PublicPoint :: SomePKPayload -> Either String B.ByteString-ed25519PublicPoint pkp =- case _pubkey pkp of- EdDSAPubKey Ed25519 point ->- maybe- (Left ("invalid Ed25519 public point for subkey " ++ renderFingerprint (fingerprint pkp)))- Right- (edPointToRawBytes point)- _ -> Left ("subkey " ++ renderFingerprint (fingerprint pkp) ++ " does not have an Ed25519 public key")--renderFingerprint :: Fingerprint -> String-renderFingerprint =- T.unpack . PPA.renderStrict . layoutPretty defaultLayoutOptions . pretty--renderKeyID :: EightOctetKeyId -> String-renderKeyID =- T.unpack . PPA.renderStrict . layoutPretty defaultLayoutOptions . pretty--knownWeakHashAlgorithms :: [HashAlgorithm]-knownWeakHashAlgorithms = [DeprecatedMD5, SHA1, RIPEMD160]--isKnownWeakHashAlgorithm :: HashAlgorithm -> Bool-isKnownWeakHashAlgorithm ha = ha `elem` knownWeakHashAlgorithms--isSubkeyRevocationSignature :: SignaturePayload -> Bool-isSubkeyRevocationSignature (SigV3 st _ _ _ _ _ _) = st == SubkeyRevocationSig-isSubkeyRevocationSignature (SigV4 st _ _ _ _ _ _) = st == SubkeyRevocationSig-isSubkeyRevocationSignature (SigV6 st _ _ _ _ _ _ _) = st == SubkeyRevocationSig-isSubkeyRevocationSignature _ = False--edPointToRawBytes :: EdPoint -> Maybe B.ByteString-edPointToRawBytes (NativeEPoint (EPoint i)) = integerToFixedBytes 32 i-edPointToRawBytes (PrefixedNativeEPoint (EPoint i)) = do- prefixed <- integerToFixedBytes 33 i- case B.uncons prefixed of- Just (0x40, raw) -> Just raw- _ -> Nothing--normalizeEd25519Secret :: B.ByteString -> Either String B.ByteString-normalizeEd25519Secret rawSecret- | B.length rawSecret == 32 = Right rawSecret- | otherwise =- Left- ("expected 32-byte Ed25519 secret seed, got " ++ show (B.length rawSecret) ++ " bytes")--putSSHString :: B.ByteString -> Put-putSSHString bs = putWord32be (fromIntegral (B.length bs)) >> putByteString bs--putSSHMpint :: Integer -> Put-putSSHMpint n- | n <= 0 = putWord32be 0- | otherwise = putSSHString encoded- where- raw = integerToUnsignedBytes n- encoded =- case B.uncons raw of- Just (firstByte, _)- | testBit firstByte 7 -> B.cons 0x00 raw- _ -> raw--integerToFixedBytes :: Int -> Integer -> Maybe B.ByteString-integerToFixedBytes width n- | n < 0 = Nothing- | B.length raw > width = Nothing- | otherwise = Just (B.replicate (width - B.length raw) 0x00 <> raw)- where- raw =- if n == 0- then B.singleton 0x00- else integerToUnsignedBytes n--integerToUnsignedBytes :: Integer -> B.ByteString-integerToUnsignedBytes n =- B.reverse $- B.unfoldr- (\value ->- if value == 0- then Nothing- else Just (fromIntegral (value .&. 0xff), value `shiftR` 8))- n--isEd25519PKA :: PubKeyAlgorithm -> Bool-isEd25519PKA pka = fromFVal pka == 27--frameSSHAgentRequest :: BL.ByteString -> BL.ByteString-frameSSHAgentRequest body =- runPut $ putWord32be (fromIntegral (BL.length body)) >> putLazyByteString body--sendAddIdentityToSSHAgent :: FilePath -> BL.ByteString -> IO ()-sendAddIdentityToSSHAgent socketPath request =- bracket- (socket AF_UNIX Stream defaultProtocol)- close- (\sock -> do- connect sock (SockAddrUnix socketPath)- NSB.sendAll sock (BL.toStrict request)- response <- readSSHAgentPacket sock- case B.uncons response of- Just (6, _) -> pure ()- Just (5, _) ->- failInject "inject-ssh-agent: ssh-agent rejected the supplied key"- Just (code, _) ->- failInject- ("inject-ssh-agent: ssh-agent returned unexpected response type " ++- show code)- Nothing ->- failInject- "inject-ssh-agent: ssh-agent returned an empty response packet")--readSSHAgentPacket :: Socket -> IO B.ByteString-readSSHAgentPacket sock = do- lenPrefix <- recvExact sock 4- let packetLen = fromIntegral (runGet getWord32be (BL.fromStrict lenPrefix))- recvExact sock packetLen--recvExact :: Socket -> Int -> IO B.ByteString-recvExact _ 0 = pure B.empty-recvExact sock remaining = go B.empty remaining- where- go acc 0 = pure acc- go acc bytesRemaining = do- chunk <- NSB.recv sock bytesRemaining- if B.null chunk- then- failInject- "inject-ssh-agent: ssh-agent socket closed while reading response"- else go (acc <> chunk) (bytesRemaining - B.length chunk)--whenEmpty :: [a] -> String -> IO ()-whenEmpty [] msg = failInject msg-whenEmpty _ _ = pure ()--failInject :: String -> IO a-failInject msg = hPutStrLn stderr msg >> exitFailure--banner' :: Handle -> IO ()-banner' h =- PPA.hPutDoc h (banner "hokey" <> hardline <> warranty "hokey" <> hardline)--sigissuer :: SignaturePayload -> Maybe EightOctetKeyId-getIssuer :: SigSubPacketPayload -> Maybe EightOctetKeyId-hashAlgo :: SignaturePayload -> HashAlgorithm-sigissuer (SigVOther 2 _) = Nothing-sigissuer SigV3 {} = Nothing-sigissuer (SigV4 _ _ _ ys xs _ _) =- listToMaybe . mapMaybe (getIssuer . _sspPayload) $ (ys ++ xs) -- FIXME: what should this be if there are multiple matches?-sigissuer (SigV6 _ _ _ _ ys xs _ _) =- listToMaybe . mapMaybe (getIssuer . _sspPayload) $ (ys ++ xs) -- FIXME: what should this be if there are multiple matches?-sigissuer (SigVOther _ _) = Nothing--getIssuer (Issuer i) = Just i-getIssuer _ = Nothing--hashAlgo (SigV3 _ _ _ _ x _ _) = x-hashAlgo (SigV4 _ _ x _ _ _ _) = x-hashAlgo (SigV6 _ _ x _ _ _ _ _) = x-hashAlgo (SigVOther _ _) = OtherHA 0+import HOpenPGP.Tools.Common.Common (banner, versioner, warranty)++import Options.Applicative.Builder+ ( command+ , footerDoc+ , headerDoc+ , info+ , prefs+ , progDesc+ , showHelpOnError+ )+import Options.Applicative.Extra (customExecParser, helper, hsubparser)+import Options.Applicative.Types (Parser)+import System.IO+ ( BufferMode(..)+ , Handle+ , hFlush+ , hSetBuffering+ , stderr+ )+import Prettyprinter (hardline)+import qualified Prettyprinter.Render.Terminal as PPA+import HOpenPGP.Tools.Hokey.Options+ ( FetchOptions+ , InjectSSHAgentOptions+ , LintOptions+ , fetchO+ , injectSSHAgentO+ , lintO+ )+import HOpenPGP.Tools.Hokey.Canonicalize (doCanonicalize)+import HOpenPGP.Tools.Hokey.Fetch (doFetch)+import HOpenPGP.Tools.Hokey.InjectSSHAgent (doInjectSSHAgent)+import HOpenPGP.Tools.Hokey.Lint (doLint)++data Command+ = CmdLint LintOptions+ | CmdCanonicalize+ | CmdFetch FetchOptions+ | CmdInjectSSHAgent InjectSSHAgentOptions++dispatch :: Command -> IO ()+dispatch (CmdFetch o) = banner' stderr >> hFlush stderr >> doFetch o+dispatch (CmdLint o) = banner' stderr >> hFlush stderr >> doLint o+dispatch CmdCanonicalize = banner' stderr >> hFlush stderr >> doCanonicalize+dispatch (CmdInjectSSHAgent o) =+ banner' stderr >> hFlush stderr >> doInjectSSHAgent o++main :: IO ()+main = do+ hSetBuffering stderr LineBuffering+ customExecParser+ (prefs showHelpOnError)+ (info+ (helper <*> versioner "hokey" <*> cmd)+ (headerDoc (Just (banner "hokey")) <>+ progDesc "hOpenPGP Key utility" <>+ footerDoc (Just (warranty "hokey")))) >>=+ dispatch++cmd :: Parser Command+cmd =+ hsubparser+ (command+ "canonicalize"+ (info+ (pure CmdCanonicalize)+ (progDesc "arrange key components in a canonical ordering")) <>+ command+ "fetch"+ (info+ (CmdFetch <$> fetchO)+ (progDesc "fetch key(s) via HKP or WKD")) <>+ command+ "inject-ssh-agent"+ (info+ (CmdInjectSSHAgent <$> injectSSHAgentO)+ (progDesc "Read exported secret key bytes, pick an auth-capable subkey, and add it to ssh-agent")) <>+ command+ "lint"+ (info (CmdLint <$> lintO) (progDesc "check key(s) for 'best practices'")))++banner' :: Handle -> IO ()+banner' h =+ PPA.hPutDoc h (banner "hokey" <> hardline <> warranty "hokey" <> hardline)+
hop.hs view
@@ -71,6 +71,7 @@ , renderSignError , signCertRevocationWithRSA , signDataWithEd25519+ , signDataWithEd25519Legacy , signDataWithEd25519V6 , signDataWithEd448 , signDataWithEd448V6@@ -153,8 +154,8 @@ import Data.Word (Word8) import qualified Data.Yaml as Y import GHC.Generics-import HOpenPGP.Tools.Armor (doDeArmor)-import HOpenPGP.Tools.Common+import HOpenPGP.Tools.Common.Armor (doDeArmor)+import HOpenPGP.Tools.Common.Common ( banner , keyMatchesEightOctetKeyId , keyMatchesFingerprint@@ -162,8 +163,8 @@ , versioner , warranty )-import HOpenPGP.Tools.Parser (parseTKExp)-import HOpenPGP.Tools.TKUtils (processTK)+import HOpenPGP.Tools.Common.Parser (parseTKExp)+import HOpenPGP.Tools.Common.TKUtils (processTK) import Paths_hopenpgp_tools (version) import System.Exit (exitFailure, exitSuccess, exitWith, ExitCode(..)) @@ -306,6 +307,7 @@ , inlineSignProfile :: Maybe String , inlineSignAs :: Maybe InlineSignMode , inlineSignKeyFiles :: [String]+ , inlineSignKeyPasswords :: [String] } data InlineDetachOptions =@@ -592,7 +594,8 @@ (option (eitherReader inlineSignModeReader) (long "as" <> metavar "DATATYPE" <> inlineSignAsHelp)) <*>- some (strArgument (metavar "KEY" <> help "signing key file(s)"))+ some (strArgument (metavar "KEY" <> help "signing key file(s)")) <*>+ many (strOption (long "with-key-password" <> metavar "PASSWORD" <> help "password for encrypted signing key")) where inlineSignAsHelp = helpDoc . Just $@@ -1398,6 +1401,8 @@ tk {_tkuKey = pToS (_tkuKey tk), _tkuSubs = map subPToS (_tkuSubs tk)} pToS (pkp, _) = (pkp, Nothing) subPToS (SecretSubkeyPkt pkp _, sigs) = (PublicSubkeyPkt pkp, sigs)+ subPToS (PublicSubkeyPkt pkp, sigs) = (PublicSubkeyPkt pkp, sigs)+ subPToS x = x doChangeKeyPassword :: ChangeKeyPasswordOptions -> IO () doChangeKeyPassword ChangeKeyPasswordOptions {..} = do@@ -2036,7 +2041,7 @@ Just (SUUnencrypted (RSAPrivateKey (RSA_PrivateKey _)) _) -> True Just (SUUnencrypted (EdDSAPrivateKey _ _) _) -> True Just (SUUnencrypted (UnknownSKey _) _) ->- isEd25519PKA (_pkalgo (fpkp k)) || isEd448PKA (_pkalgo (fpkp k))+ isEd25519PKA (_pkalgo (fpkp k)) || isEdDSAPKA (_pkalgo (fpkp k)) || isEd448PKA (_pkalgo (fpkp k)) _ -> False canSignDataUsage keyFlags = S.null keyFlags || S.member SignDataKey keyFlags @@ -2071,6 +2076,10 @@ Left err -> failWith BadData (ctx ++ " failed: bad Ed25519 secret key: " ++ show err) Right sk | _keyVersion signerPKP == V6 -> signEd25519 sk+ | isEdDSAPKA (_pkalgo signerPKP) ->+ case signDataWithEd25519Legacy st sk hsd usd payload of+ Left err' -> failWith BadData (ctx ++ " failed: " ++ renderSignError err')+ Right sig -> pure sig | otherwise -> case signDataWithEd25519 st sk hsd usd payload of Left err' -> failWith BadData (ctx ++ " failed: " ++ renderSignError err')@@ -2093,6 +2102,10 @@ Left err -> failWith BadData (ctx ++ " failed: bad Ed25519 secret key: " ++ show err) Right sk | _keyVersion signerPKP == V6 -> signEd25519 sk+ | isEdDSAPKA (_pkalgo signerPKP) ->+ case signDataWithEd25519Legacy st sk hsd usd payload of+ Left err' -> failWith BadData (ctx ++ " failed: " ++ renderSignError err')+ Right sig -> pure sig | otherwise -> case signDataWithEd25519 st sk hsd usd payload of Left err' -> failWith BadData (ctx ++ " failed: " ++ renderSignError err')@@ -2180,10 +2193,10 @@ BadData (ctx ++ " failed: unable to determine RSA key size: " ++ err) -isEd25519PKA :: PubKeyAlgorithm -> Bool+-- FIXME: clean this up+isEd25519PKA, isEd448PKA, isEdDSAPKA :: PubKeyAlgorithm -> Bool isEd25519PKA pka = fromFVal pka == 27--isEd448PKA :: PubKeyAlgorithm -> Bool+isEdDSAPKA pka = fromFVal pka == 22 isEd448PKA pka = fromFVal pka == 28 selectSigningHash :: [FunKey] -> [HashAlgorithm] -> [HashAlgorithm] -> HashAlgorithm@@ -4532,7 +4545,9 @@ mbs <- runConduitRes $ CB.sourceHandle stdin .| CL.consume when (inlineMode /= InlineSignAsBinary) $ ensureUTF8TextInput "inline-sign" (BL.fromChunks mbs)- ks <- mapM grabKey inlineSignKeyFiles+ signingPasswordsRaw <- loadPasswordFiles "inline-sign" "--with-key-password" inlineSignKeyPasswords+ let signingPasswords = concatMap passwordRetryCandidates signingPasswordsRaw+ ks <- loadSigningKeys inlineSignKeyFiles signingPasswords processedKeys <- mapM (normalizeSigningKey pt) ks let ts = ThirtyTwoBitTimeStamp (floor pt) payloadRaw = BL.fromChunks mbs@@ -4608,7 +4623,7 @@ Just (SUUnencrypted (RSAPrivateKey (RSA_PrivateKey _)) _) -> True Just (SUUnencrypted (EdDSAPrivateKey _ _) _) -> True Just (SUUnencrypted (UnknownSKey _) _) ->- isEd25519PKA (_pkalgo (fpkp k)) || isEd448PKA (_pkalgo (fpkp k))+ isEd25519PKA (_pkalgo (fpkp k)) || isEdDSAPKA (_pkalgo (fpkp k)) || isEd448PKA (_pkalgo (fpkp k)) _ -> False -- Legacy alias.
hopenpgp-tools.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: hopenpgp-tools-version: 0.25+version: 0.25.1 synopsis: hOpenPGP-based command-line tools description: command-line tools for performing some OpenPGP-related operations homepage: https://salsa.debian.org/clint/hOpenPGP-tools@@ -20,29 +20,29 @@ autogen-modules: Paths_hopenpgp_tools build-depends: base >= 4.15 && < 5 , aeson- , binary >= 0.6.4.0+ , binary >= 0.6.4 , binary-conduit , bytestring- , conduit >= 1.3.0+ , conduit >= 1.3 , errors- , hOpenPGP >= 3 && < 3.1+ , hOpenPGP >= 3.0.2 && < 3.1 , lens- , optparse-applicative >= 0.18.1.0- , prettyprinter >= 1.7.0+ , optparse-applicative >= 0.18.1+ , prettyprinter >= 1.7 , text , transformers >= 0.4 , yaml ghc-options: -Wall- other-modules: HOpenPGP.Tools.Common+ other-modules: HOpenPGP.Tools.Common.Common , Paths_hopenpgp_tools executable hot import: deps main-is: hot.hs autogen-modules: Paths_hopenpgp_tools- other-modules: HOpenPGP.Tools.Armor- , HOpenPGP.Tools.Lexer- , HOpenPGP.Tools.Parser+ other-modules: HOpenPGP.Tools.Common.Armor+ , HOpenPGP.Tools.Common.Lexer+ , HOpenPGP.Tools.Common.Parser build-depends: array , conduit-extra >= 1.1 , monad-loops@@ -53,9 +53,14 @@ executable hokey import: deps main-is: hokey.hs- other-modules: HOpenPGP.Tools.HKP- , HOpenPGP.Tools.TKUtils- , HOpenPGP.Tools.WKD+ other-modules: HOpenPGP.Tools.Common.HKP+ , HOpenPGP.Tools.Common.TKUtils+ , HOpenPGP.Tools.Common.WKD+ , HOpenPGP.Tools.Hokey.Options+ , HOpenPGP.Tools.Hokey.Canonicalize+ , HOpenPGP.Tools.Hokey.Fetch+ , HOpenPGP.Tools.Hokey.InjectSSHAgent+ , HOpenPGP.Tools.Hokey.Lint build-depends: base16-bytestring , conduit-extra >= 1.1 , containers@@ -76,13 +81,13 @@ executable hkt import: deps main-is: hkt.hs- other-modules: HOpenPGP.Tools.Lexer- , HOpenPGP.Tools.Parser+ other-modules: HOpenPGP.Tools.Common.Lexer+ , HOpenPGP.Tools.Common.Parser build-depends: array , containers , conduit-extra >= 1.1 , directory- , fgl >= 5.5.4.0+ , fgl >= 5.5.4 , graphviz , ixset-typed , monad-loops@@ -95,10 +100,10 @@ executable hop import: deps main-is: hop.hs- other-modules: HOpenPGP.Tools.Armor- , HOpenPGP.Tools.Lexer- , HOpenPGP.Tools.Parser- , HOpenPGP.Tools.TKUtils+ other-modules: HOpenPGP.Tools.Common.Armor+ , HOpenPGP.Tools.Common.Lexer+ , HOpenPGP.Tools.Common.Parser+ , HOpenPGP.Tools.Common.TKUtils build-depends: array , conduit-extra >= 1.1 , containers@@ -124,4 +129,4 @@ source-repository this type: git location: https://salsa.debian.org/clint/hopenpgp-tools.git- tag: hopenpgp-tools/0.25+ tag: hopenpgp-tools/0.25.1
hot.hs view
@@ -1,5 +1,5 @@ -- hot.hs: hOpenPGP Tool--- Copyright © 2012-2022 Clint Adams+-- Copyright © 2012-2026 Clint Adams -- -- vim: softtabstop=4:shiftwidth=4:expandtab --@@ -42,9 +42,9 @@ import Data.Conduit.Serialization.Binary (conduitGet, conduitPut) import Data.Void (Void) import qualified Data.Yaml as Y-import HOpenPGP.Tools.Armor (doDeArmor)-import HOpenPGP.Tools.Common (banner, prependAuto, versioner, warranty)-import HOpenPGP.Tools.Parser (parsePExp)+import HOpenPGP.Tools.Common.Armor (doDeArmor)+import HOpenPGP.Tools.Common.Common (banner, prependAuto, versioner, warranty)+import HOpenPGP.Tools.Common.Parser (parsePExp) import System.IO ( BufferMode(..)