{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ViewPatterns #-} module Kiki ( module Kiki , setVerifyFlag ) where import qualified Codec.Encryption.OpenPGP.ASCIIArmor as ASCIIArmor import Codec.Encryption.OpenPGP.ASCIIArmor.Types import Control.Applicative import Control.Exception import Control.Monad import qualified Crypto.Hash as C import Data.ASN1.BinaryEncoding import Data.ASN1.Encoding import Data.ASN1.Types import Data.Binary import Data.Bool import Data.Coerce import Data.Char import Data.Functor import Data.List import Data.Maybe import Data.OpenPGP import Data.OpenPGP.Util import Data.Ord import qualified Data.Traversable as T (mapM) import System.Directory import System.FilePath.Posix as FilePath import System.IO import System.IO.Error import System.IO.Temp import System.Posix.Files import System.Posix.Types (FileMode) import System.Posix.IO as Posix (createPipe) import System.Posix.User import Data.ByteArray.Encoding import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy.Char8 as Char8 import qualified Data.Map.Strict as Map import GHC.Stack import Network.Socket import qualified SSHKey as SSH import CommandLine import DotLock import GnuPGAgent (Query (..)) -- import qualified IntMapClass as I import KeyRing hiding (pemFromPacket) import KeyDB import KeyRing.BuildKeyDB (gpgipv6addr, Hostnames, allNames) import TimeUtil withAgent :: [PassphraseSpec] -> [PassphraseSpec] withAgent [] = [PassphraseAgent] withAgent ps = ps ciphername :: SymmetricAlgorithm -> String ciphername Unencrypted = "-" ciphername TripleDES = "3des" ciphername (SymmetricAlgorithm w8) = "cipher-"++show w8 ciphername c = map toLower $ show c cipherFromString :: String -> SymmetricAlgorithm cipherFromString "clear" = Unencrypted cipherFromString "unencrypted" = Unencrypted cipherFromString s = case filter ( (== s) . ciphername) ciphers of x:_ -> x -- _ | all isHexDigit s -> unhex s _ -> error $ "known ciphers: "++unwords (map ciphername ciphers) {- where unhex hx = case convertFromBase Base16 (S8.pack hx) of Left e -> do -- Useful for debugging but insecure generally ;) -- putStrLn $ "convertFromBase error for input "++show hx++": "++show e return Nothing Right bs -> return $ Just $ S8.unpack bs -} ciphers :: [SymmetricAlgorithm] ciphers = takeWhile notFallback $ map toEnum $ [0..4]++[7..] where notFallback (SymmetricAlgorithm _) = False notFallback _ = True -- | -- Regenerate /var/cache/kiki refresh :: (FilePath -> FilePath) -> CommonArgsParsed -> IO () refresh root homepass = do let homepass' = homepass { cap_homespec = fmap root (cap_homespec homepass) } KikiResult r report <- runKeyRing $ minimalOp False homepass' let mroot = case root "" of "/" -> Nothing "" -> Nothing pth -> Just pth case r of KikiSuccess rt -> refreshCache rt mroot _ -> return () -- XXX: silent fail? data CommonArgsParsed = CommonArgsParsed { cap_homespec :: Maybe String , cap_passfd :: Maybe InputFile , cap_fpstyle :: FingerprintStyle } data FingerprintStyle = FingerprintAuto | Fingerprint5 deriving (Eq,Ord,Show) instance Read FingerprintStyle where readsPrec _ s = case break isSpace s of ("auto",t) -> [(FingerprintAuto, drop 1 t)] ("5",t) -> [(Fingerprint5, drop 1 t)] _ -> [] streaminfo :: StreamInfo streaminfo = StreamInfo { fill = KF_None , spill = KF_None , typ = KeyRingFile , initializer = NoCreate , access = AutoAccess , transforms = [] } minimalOp :: Bool -> CommonArgsParsed -> KeyRingOperation minimalOp isHomeless cap = op where streaminfo = StreamInfo { fill = KF_None , typ = KeyRingFile , spill = KF_All , initializer = NoCreate , access = AutoAccess , transforms = [] } op = KeyRingOperation { opFiles = if isHomeless then Map.empty else Map.fromList $ [ ( HomeSec, streaminfo { access = Sec }) , ( HomePub, streaminfo { access = Pub }) ] , opPassphrases = withAgent $ do pfile <- maybeToList (cap_passfd cap) return $ PassphraseSpec Nothing Nothing pfile , opTransforms = [] , opHome = cap_homespec cap , preferredPGPVersion = 4 } run :: [String] -> Args (IO ()) -> IO () run args x = case runArgs (parseInvocation (uncurry fancy kikiOptions "") args) x of Left e -> hPutStrLn stderr $ usageErrorMessage e Right io -> io outputReport :: [(FilePath, KikiReportAction)] -> IO () outputReport report = do forM_ report $ \(fname,act) -> do putStrLn $ fname ++ ": " ++ reportString act spawn :: CommonArgsParsed -> SymmetricAlgorithm -> FilePath -> IO () spawn cmn cipher path = do putStrLn "TODO: implement keygen.sh replacement.sh" mkdirFor :: FilePath -> IO () mkdirFor path = do let dir = takeDirectory path createDirectoryIfMissing True dir -- | Useful default KeyRingFile StreamInfo. strm :: StreamInfo strm = StreamInfo { typ = KeyRingFile , fill = KF_None , spill = KF_All , access = AutoAccess , initializer = NoCreate , transforms = [] } -- | Convenience constructor for StreamInfo buildStreamInfo :: KeyFilter -> FileType -> StreamInfo buildStreamInfo rtyp ftyp = strm { typ = ftyp , fill = rtyp } -- | Convenience constuctor for Streaminfo generating a tagged subkey. peminfo :: Int -- ^ bits -> String -- ^ subkey tag. -> StreamInfo peminfo bits usage = StreamInfo { typ = PEMFile , fill = KF_None -- KF_Match usage , spill = KF_Match usage , access = Sec , initializer = Internal (GenRSA $ bits `div` 8) , transforms = [] } importAndRefresh :: Word8 -> (FilePath -> FilePath) -> CommonArgsParsed -> SymmetricAlgorithm -> IO () importAndRefresh pgpver root cmn cipher = do let rootdir = do guard (root "x" /= "x") Just $ root "" me <- getEffectiveUserID let noChrootArg = rootdir == Nothing bUnprivileged = (me/=0) && noChrootArg bool id (error "--chroot requires an argument") (rootdir==Just "") $ do let homespec = mplus (slash <$> rootdir <*> cap_homespec cmn) (fmap (++"/root/.gnupg") rootdir) passfd = cap_passfd cmn (home,secring,pubring,mbwk) <- unconditionally $ getHomeDir homespec osHomeDir <- if bUnprivileged then getHomeDirectory else return "/root" old_umask <- setFileCreationMask 0o077 -- Keyring files need to be created with proper mask. gotsec <- doesFileExist secring (torgen,pwds) <- bool id (const $ return (Generate 0 $ GenRSA $ 1024 `div` 8, [])) gotsec $ do -- We have no secring.gpg (and thus no master key). -- Since 'runKeyRing' cannot currently cope with this situation, we will -- generate a master-key and very minimal secring.gpg file. master_un <- generateKey (GenRSA $ 4096 `div` 8 ) <&> \k -> MappedPacket (k { is_subkey = False -- Set as master-key. , version = pgpver }) -- Set pgp packet version. Map.empty -- Packet occurs in no files. -- The user may desire the master key is encrypted on disk but this -- requires a password prompt. In order to have a decent prompt, it'd -- be nice if we could display the .onion hostname for the key. -- Therefore, we generate the tor key early. tor_un <- fmap (\k -> k { version = pgpver }) $ generateKey (GenRSA $ 1024 `div` 8 ) -- However, we'll postpone writing the tor key to the keyring and -- instead have the later all-in-one call to runKeyRing take care of -- it. That interface does not currently provide a way to accept -- in-memory input, so we'll create a fifo stream for it to read the -- key in PEM format. (read_tor,write_tor) <- Posix.createPipe rs <- writeKeyToFile (streaminfo { typ = PEMFile , access = Sec , spill = KF_Match "tor" , fill = KF_All }) (FileDesc write_tor) tor_un -- -- Currently disabled: show warnings and errors from the PEM generation. -- outputReport $ map (first show) rs let ctx = InputFileContext secring pubring -- Here we encrypt the master-key if neccessary. If no --passphrase-fd option -- was used, the user will receive prompts from gpg-agent. (master0,transcoder) <- do let main_passwds = withAgent $ do pfd <- maybeToList passfd return $ PassphraseSpec Nothing Nothing pfd uidentry = Map.singleton (keykey $ packet master_un) $ master_un { packet = Query (packet master_un) (torUIDFromKey tor_un) Nothing } cipher's2k <- do IteratedSaltedS2K _ salt _ <- randomS2K SHA1 return $ (cipher {- AES128 -}, IteratedSaltedS2K SHA1 salt (15 * 2^19)) transcoder <- makeMemoizingDecrypter main_passwds ctx (Just master_un, uidentry) master <- transcoder cipher's2k master_un return (master,transcoder) -- Finally, we write-out the secring.gpg file. case master0 of KikiSuccess master -> do mkdirFor secring writeInputFileL ctx HomeSec $ encode $ Message [master] putStrLn $ "Wrote master key." return (FileDesc read_tor, [PassphraseMemoizer transcoder]) er -> do hPutStrLn stderr ("warning: " ++ errorString er) hPutStrLn stderr "warning: keys will not be encrypted."; mkdirFor secring writeInputFileL ctx HomeSec $ encode $ Message [packet master_un] putStrLn $ "Wrote unencrypted master key." return (FileDesc read_tor, []) -- If the public ring does not exist, then creating an empty file is -- sufficient to satisfy 'runKeyRing'. However, as we've already generated -- a key above, GnuPG will not like the unsynced state we are leaving these -- files. It's important at this point that, 'runKeyRing' actually occurs -- to fix things up. gotpub <- doesFileExist pubring when (not gotpub) $ do mkdirFor pubring writeInputFileL (InputFileContext secring pubring) HomePub ( encode $ Message [] ) setFileCreationMask old_umask -- We're done creating keyring files, so restore umask. let sshcpath = fromMaybe "" rootdir ++ osHomeDir ++ ".ssh/id_rsa" sshspath = fromMaybe "" rootdir ++ "/etc/ssh/ssh_host_rsa_key" mktorkey = ( torgen , case torgen of FileDesc _ -> StreamInfo { typ = PEMFile , fill = KF_Match "tor" , spill = KF_Match "tor" , access = Sec , initializer = NoCreate , transforms = [] } _ -> strm { spill = KF_Match "tor" }) op = KeyRingOperation { opFiles = Map.fromList $ [ ( HomeSec, buildStreamInfo KF_All KeyRingFile ) , ( HomePub, (buildStreamInfo KF_All KeyRingFile) { access = Pub } ) , mktorkey , ( Generate 1 (GenRSA (2048 `div` 8)), strm { spill = KF_Match "ipsec" }) , ( ArgFile sshcpath, (peminfo 2048 "ssh-client") ) , ( ArgFile sshspath, (peminfo 2048 "ssh-server") ) , ( Generate 2 (GenRSA (4096 `div` 8)), strm { spill = KF_Match "encrypt" }) --, ( Generate 3 GenEd25519, strm { spill = KF_Match "sign" }) --, ( Generate 4 GenCv25519, strm { spill = KF_Match "tox-id" }) ] , opPassphrases = withAgent $ pwds ++ do pfd <- maybeToList passfd return $ PassphraseSpec Nothing Nothing pfd , opHome = homespec , opTransforms = [] , preferredPGPVersion = pgpver } nop = KeyRingOperation { opFiles = Map.fromList -- It's too late for a true no-op, -- so we'll sync the keyrings at least. [ ( HomeSec, buildStreamInfo KF_All KeyRingFile ) , ( HomePub, (buildStreamInfo KF_All KeyRingFile) { access = Pub } ) , mktorkey -- We'll also add the torkey and UID since -- otherwise this will be a strange keyring. ] , opPassphrases = withAgent $ do pfd <- maybeToList passfd return $ PassphraseSpec Nothing Nothing pfd , opHome=homespec , opTransforms = [] , preferredPGPVersion = pgpver } when bUnprivileged $ do hPutStrLn stderr "Insufficient privilege generating system service keys." -- Run the all-in-one operation that generates or imports all subkeys. KikiResult rt report <- runKeyRing (if bUnprivileged then nop else op) outputReport report rt <- case rt of BadPassphrase -> error "Operation requires correct passphrase. (Hint: Use --passphrase-fd=0 to input it on stdin.)" _ -> unconditionally $ return rt -- Finally, we update /var/cache/kiki. when (not bUnprivileged) $ refreshCache rt rootdir data Peer = Peer { peerHostnames :: Hostnames , kd :: KeyData } addr :: Peer -> SockAddr addr = gpgipv6addr . peerHostnames newtype IpsecPeerConfig = IpsecPeerConfig Char8.ByteString -- Installs the cert file for the peer to the filesystem, and returns an -- ipsec.conf snippet configuring the peer and referencing the installed cert -- file. installIpsecPeerCertificate :: FileWriter -> Peer -> IO IpsecPeerConfig installIpsecPeerCertificate fw p@Peer{kd} = IpsecPeerConfig <$> fromMaybe "" <$> do forM (listToMaybe ipsecs) $ \k -> do flip (either warn') (pemFromPacket k :: Either String String) $ \pem -> do case sshs of (sshkey:_) -> do let theirHostname = ResolvableHostname $ sshKeyToHostname sshkey write fw (peerCertPath p) pem return $ strongswanPeerConfiguration p theirHostname _ -> error "fuck." where warn' x = warn x >> return Char8.empty their_master = packet $ keyMappedPacket kd :: Packet -- We find all cross-certified ipsec keys for the given cross-certified onion name. ipsecs :: [Packet] ipsecs = sortOn (Down . timestamp) $ getSubkeys CrossSigned their_master (keySubKeys kd) "ipsec" -- ++ getSubkeys CrossSigned their_master (keySubKeys kd) "strongswan" sshs :: [Packet] sshs = sortOn (Down . timestamp) $ getSubkeys CrossSigned their_master (keySubKeys kd) "ssh-server" ipsecPath :: String -> Char8.ByteString -> String ipsecPath theDirName theBaseName = "ipsec.d/" ++ theDirName ++ "/" ++ Char8.unpack theBaseName ipsecKeyPath :: MyIdentity -> FilePath ipsecKeyPath (MyIdentity (Char8.pack . showA -> addr) _) = ipsecPath "private" (addr <> ".pem") ipsecCertPath :: MyIdentity -> FilePath ipsecCertPath (MyIdentity (Char8.pack . showA -> addr) _) = ipsecPath "certs" (addr <> ".pem") peerCertPath :: Peer -> FilePath peerCertPath = ipsecPath "certs" . coerce . peerCertificateName makeFileWriter :: (FilePath -> FilePath) -> (IO ()) -> FileWriter makeFileWriter p c = FileWriter { pathMaker = p , fileWriterCommit = c , write = write' writeFile , writeL = write' L.writeFile , writeL077 = \f bs -> do old_umask <- setFileCreationMask 0o077 write' L.writeFile f bs setFileCreationMask old_umask } where write' wr (p -> f) bs = do createDirectoryIfMissing True $ takeDirectory f wr f bs data FileWriter = FileWriter { pathMaker :: FilePath -> FilePath , fileWriterCommit :: IO () , write :: FilePath -> String -> IO () , writeL :: FilePath -> Char8.ByteString -> IO () , writeL077 :: FilePath -> Char8.ByteString -> IO FileMode } getMkPathAndCommit :: FilePath -> IO (FileWriter) getMkPathAndCommit destdir = do let cachedir = takeDirectory destdir unslash ('/':xs) = xs unslash xs = xs timeout = -1 -- TODO: set milisecond timeout on dotlock createDirectoryIfMissing True cachedir tmpdir <- createTempDirectory cachedir (takeBaseName destdir ++ ".") createSymbolicLink (makeRelative cachedir tmpdir) (tmpdir ++ ".link") lock <- dotlock_create destdir 0 T.mapM (flip dotlock_take timeout) lock let mkpath pth = tmpdir unslash (makeRelative destdir pth) commit = do oldcommit <- (Just <$> readSymbolicLink destdir) `catch` \e -> do when (not $ isDoesNotExistError e) $ warn (show e) return Nothing -- Note: Files not written to are considered deleted, -- otherwise call readyReadBeforeWrite on them. rename (tmpdir ++ ".link") destdir er <- T.mapM dotlock_release lock void $ T.mapM removeDirectoryRecursive (FilePath.combine cachedir <$> oldcommit) -- Present transaction is Write only (or Write-Before-Read) which is fine. -- If ever Read-Before-Write is required, uncomment and use: -- let readyReadBeforeWrite pth = do -- let copyIt = do -- createDirectoryIfMissing True (takeDirectory (mkpath pth)) -- copyFile (destdir unslash (makeRelative destdir pth) (mkpath pth) -- doesFileExist (mkpath pth) >>= flip when copyIt -- return (mkpath pth) return $ makeFileWriter mkpath commit generateHostsFile :: FileWriter -> KeyRingRuntime -> IO () generateHostsFile fw rt = do let hostspath = pathMaker fw "hosts" op = KeyRingOperation { opFiles = Map.fromList $ [ ( HomePub, streaminfo { typ=KeyRingFile, spill=KF_All, access=Pub } ) , ( ArgFile hostspath, streaminfo { typ=Hosts, spill=KF_None, fill=KF_All, access=Pub } ) ] , opPassphrases = [] , opHome = Just $ takeDirectory (rtPubring rt) , opTransforms = [] , preferredPGPVersion = 4 -- Does not matter (no fill of pgp files). } KikiResult _ report <- runKeyRing op outputReport report getSshKnownHosts :: Peer -> Char8.ByteString getSshKnownHosts peer@Peer{kd} = Char8.unlines taggedblobs where their_master = packet $ keyMappedPacket kd sshs :: [Packet] sshs = sortOn (Down . timestamp) $ getSubkeys CrossSigned their_master (keySubKeys $ kd) "ssh-server" blobs = mapMaybe sshblobFromPacketL sshs taggedblobs = do n <- allNames $ peerHostnames peer map ((coerce n <> " ") <>) blobs data MyIdentity = MyIdentity { myGpgAddress :: SockAddr, myGpgKeyGrip :: String } installIpsecConf :: FileWriter -> MyIdentity -> [Peer] -> IO () installIpsecConf fw MyIdentity{myGpgAddress} cs = do snippets <- mapM (coerce . installIpsecPeerCertificate fw) cs writeL fw "ipsec.conf" . Char8.unlines $ [ "conn %default" , " ikelifetime=60m" , " keylife=20m" , " rekeymargin=3m" , " keyingtries=%forever" , " keyexchange=ikev2" , " dpddelay=10s" , " dpdaction=restart" , " left=%defaultroute" , " leftsubnet=" <> Char8.pack (showA myGpgAddress) <> "/128" , " leftauth=pubkey" , " leftid=" <> Char8.pack (showA myGpgAddress) , " leftsigkey=" <> Char8.pack (showA myGpgAddress) <> ".pem" , " leftikeport=4500" , " rightikeport=4500" , " right=%any" , " rightauth=pubkey" , " type=tunnel" , " auto=route" , "" ] ++ filter (not . Char8.null) snippets getMyIdentity :: KeyRingRuntime -> Maybe MyIdentity getMyIdentity rt = do wk <- rtWorkingKey rt wkaddr <- gpgipv6addr . getHostnames <$> lookupKeyData (keykey wk) (rtKeyDB rt) return $ MyIdentity wkaddr (show $ fingerprint wk) refreshCache :: KeyRingRuntime -> Maybe FilePath -> IO () refreshCache rt rootdir = do fw <- getMkPathAndCommit (fromMaybe "" rootdir ++ "/var/cache/kiki/config") generateHostsFile fw rt fromMaybe (error "No working key.") $ do myId <- getMyIdentity rt Just $ do let exportOp = passphrases <> pemSecrets <> minimalOp False (CommonArgsParsed (Just $ takeDirectory $ rtPubring rt) Nothing FingerprintAuto) where passphrases = mempty { opPassphrases = [PassphraseMemoizer (rtPassphrases rt)] } pemSecrets = mempty { opFiles = Map.fromList [ send "ipsec" (ipsecKeyPath myId) "missing ipsec key?" , send "ssh-client" ("root/.ssh/id_rsa") "missing ssh-client key?" , send "ssh-server" ("ssh_host_rsa_key") "missing ssh host key?" , send "tor" ("tor/private_key") "missing tor key?" ] } send usage path warning = ( ArgFile (pathMaker fw path), StreamInfo { typ = PEMFile , fill = KF_Match usage , spill = KF_None , access = Sec , initializer = WarnMissing warning , transforms = [] }) KikiResult rt' report <- runKeyRing exportOp outputReport report -- outputReport $ map (first $ resolveForReport ctx) $ filter ((/=ExportedSubkey) . snd) report rt'' <- rethrowKikiErrors rt' writePublicKeyFiles rt'' fw myId rethrowKikiErrors :: HasCallStack => KikiCondition a -> IO a rethrowKikiErrors BadPassphrase = error "Operation requires correct passphrase. (Hint: Use --passphrase-fd=0 to input it on stdin.)" rethrowKikiErrors rt = unconditionally $ return rt newtype UidHostname = UidHostname Char8.ByteString newtype ResolvableHostname = ResolvableHostname Char8.ByteString listPeers :: KeyRingRuntime -> [Peer] listPeers rt = map (uncurry Peer) . filter notme . mapMaybe namedContact . keyData . rtKeyDB $ rt where kk = keykey (fromJust $ rtWorkingKey rt) notme (_,kd) = keykey (keyPacket kd) /= kk namedContact kd = do let h = getHostnames kd _ <- listToMaybe $ allNames h return (h, kd) writePublicKeyFiles :: KeyRingRuntime -> FileWriter -> MyIdentity -> IO () writePublicKeyFiles rt fw myId@(MyIdentity _ grip) = do -- Finally, export public keys if they do not exist. either warn (write fw "root/.ssh/id_rsa.pub") $ show_ssh' "ssh-client" grip (rtKeyDB rt) either warn (write fw "ssh_host_rsa_key.pub") $ show_ssh' "ssh-server" grip (rtKeyDB rt) either warn (write fw $ ipsecCertPath myId) $ show_pem' "ipsec" grip (rtKeyDB rt) pemFromPacket let cs = listPeers rt known_hosts = L.concat $ map getSshKnownHosts $ cs writeL fw "ssh_known_hosts" known_hosts installIpsecConf fw myId cs fileWriterCommit fw sshKeyToHostname :: Packet -> Char8.ByteString sshKeyToHostname sshkey = do case rsaKeyFromPacket sshkey of Just (RSAKey (MPI n) (MPI e)) -> do let blob = SSH.sshrsa e n sum = C.hashlazy blob :: C.Digest C.SHA256 Char8.fromStrict (convertToBase Base16 sum) <> ".ssh-rsa.cryptonomic.net" Nothing -> "" peerConnectionName :: Peer -> Char8.ByteString peerConnectionName = coerce . peerAddress peerCertificateName :: Peer -> Char8.ByteString peerCertificateName = (<> ".pem") . coerce . peerAddress peerAddress :: Peer -> Char8.ByteString peerAddress = Char8.pack . showA . addr strongswanPeerConfiguration :: Peer -> ResolvableHostname -> Char8.ByteString strongswanPeerConfiguration peer (ResolvableHostname rightip) = Char8.unlines [ "conn " <> peerConnectionName peer , " right=" <> rightip , " rightsubnet=" <> peerAddress peer <> "/128" , " rightauth=pubkey" , " rightid=" <> peerAddress peer , " rightsigkey=" <> peerCertificateName peer ] -- conn hiotuxliwisbp6mi.onion -- right=%hiotuxliwisbp6mi.onion.ipv4 -- rightsubnet=fdcc:76c8:cb34:74e6:2aa3:cb39:abc8:d403/128 -- rightauth=pubkey -- rightid=fdcc:76c8:cb34:74e6:2aa3:cb39:abc8:d403 -- rightrsasigkey=hiotuxliwisbp6mi.onion.pem showA :: SockAddr -> String showA addr = if null bracket then pre else drop 1 pre where (pre,bracket) = break (==']') (show addr) pemFromPacket :: Monad m => Packet -> m String pemFromPacket k = do let rsa = pkcs8 . fromJust $ rsaKeyFromPacket k der = encodeASN1 DER (toASN1 rsa []) qq = S8.unpack $ convertToBase Base64 (L.toStrict der) return $ writePEM PemPublicKey qq -- ("TODO "++show keyspec) show_pem :: String -> String -> KeyDB -> IO () show_pem keyspec wkgrip db = either warn putStrLn $ show_pem' keyspec wkgrip db pemFromPacket show_pem' :: String -> String -> KeyDB -> (Packet -> Either String b) -> Either String b show_pem' keyspec wkgrip db keyfmt = do let s = parseSpec wkgrip keyspec flip (maybe . Left $ keyspec ++ ": not found") (selectPublicKey s db) keyfmt warn :: String -> IO () warn str = hPutStrLn stderr str show_sshfp :: String -> String -> KeyDB -> IO () show_sshfp keyspec wkgrip db = do let s = parseSpec wkgrip keyspec case selectPublicKey s db of Nothing -> hPutStrLn stderr $ keyspec ++ ": not found" Just k -> Char8.putStrLn $ sshKeyToHostname k show_ssh :: String -> String -> KeyDB -> IO () show_ssh keyspec wkgrip db = either warn putStrLn $ show_ssh' keyspec wkgrip db show_ssh' :: String -> String -> KeyDB -> Either String String show_ssh' keyspec wkgrip db = do let s = parseSpec wkgrip keyspec flip (maybe . Left $ keyspec ++ ": not found") (selectPublicKey s db) $ return . sshblobFromPacket -- | -- interpolate %var patterns in a string. interp :: Map.Map String String -> String -> String interp vars raw = es >>= interp1 where gs = groupBy (\_ c -> c/='%') raw es = dropWhile null $ gobbleEscapes ("":gs) where gobbleEscapes :: [String] -> [String] gobbleEscapes (a:"%":b:bs) = (a++b) : gobbleEscapes bs gobbleEscapes (g:gs) = g : gobbleEscapes gs gobbleEscapes [] = [] interp1 ('%':'(':str) = fromMaybe "" (Map.lookup key vars) ++ drop 1 rest where (key,rest) = break (==')') str interp1 plain = plain sshblobFromPacket :: Packet -> String sshblobFromPacket k = Char8.unpack $ fromJust $ sshblobFromPacketL k sshblobFromPacketL :: Packet -> Maybe Char8.ByteString sshblobFromPacketL k = do RSAKey (MPI n) (MPI e) <- rsaKeyFromPacket k return $ SSH.keyblob (n,e) {- replaceSshServerKeys root cmn = do let homepass' = cmn { cap_homespec = fmap root (cap_homespec cmn) } replaceSSH op = op { opFiles = files } where files = Map.adjust delssh HomeSec $ Map.adjust delssh HomePub $ Map.insert (ArgFile $ root "/etc/ssh/ssh_host_rsa_key") strm $ opFiles op strm = streaminfo { typ = PEMFile, spill = KF_Match "ssh-server", access = Sec } delssh strm = strm { transforms = DeleteSubkeyByUsage "ssh-server" : transforms strm , fill = KF_All } KikiResult r report <- runKeyRing $ minimalOp False homepass' case r of KikiSuccess rt -> Kiki.refreshCache rt $ case root "" of "/" -> Nothing "" -> Nothing pth -> Just pth err -> hPutStrLn stderr $ errorString err -} slash :: String -> String -> String slash "/" ('/':xs) = '/':xs slash "" ('/':xs) = '/':xs slash "" xs = '/':xs slash (y:ys) xs = y:slash ys xs dashdashPGPVersion :: Args Word8 dashdashPGPVersion = liftA2 (\is4 is5 -> if is5 then 5 else if is4 then 4 else defaultV) (flag "-4") (flag "-5") where defaultV = 4 -- Use v4 packets by default for now. dashdashChroot :: Args (FilePath -> FilePath) dashdashChroot = pure (\r a -> slash r a) <*> arg "--chroot" <|> pure id dashdashHomedir :: Args CommonArgsParsed dashdashHomedir = CommonArgsParsed <$> optional (arg "--homedir") <*> optional (FileDesc <$> read <$> arg "--passphrase-fd") <*> (fromMaybe FingerprintAuto <$> optional (read <$> arg "--fingerprint")) dashdashCipher :: Args SymmetricAlgorithm dashdashCipher = fromMaybe CAST5 <$> optional (cipherFromString <$> arg "--cipher") kikiOptions :: ( [(String,Int)], [String] ) kikiOptions = ( ss, ps ) where ss = [("--chroot",1),("--passphrase-fd",1),("--homedir",1),("--cipher",1)] ps = [] verifyFile :: Bool -> CommonArgsParsed -> [FilePath] -> FilePath -> IO () verifyFile isHomeless cap keyrings filename = do let mop = minimalOp isHomeless cap KikiResult r report <- runKeyRing mop { opFiles = opFiles mop `Map.union` Map.fromList [ (ArgFile f, strm { access = Pub }) | f <- keyrings ] } case r of KikiSuccess rt -> go rt err -> hPutStrLn stderr $ errorString err where go :: KeyRingRuntime -> IO () go rt = do bs <- L.readFile filename case ASCIIArmor.decodeLazy bs of Right (ClearSigned hashes txt (Armor ArmorSignature _ sig):_) -> case parsePackets sig of Right sigs -> do let over = DataSignature lit sigs lit = LiteralDataPacket { format = error "format" :: Char -- TODO , filename = filename , timestamp = error "timestamp" :: Word32 -- TODO , content = txt } -- TODO: Remove this take 1 after optimizing 'candidateSignerKeys' tentativeTake1 xs = take 1 xs keys = concatMap (candidateSignerKeys (rtKeyDB rt)) $ tentativeTake1 sigs good = verify (Message keys) over putStrLn $ unwords [ "verifyFile:" , show (length $ signatures_over good) , "good of" , show (length $ signatures_over over) , "signatures." ] -- when (null (signatures_over good)) $ do -- L.putStrLn txt rs -> do hPutStrLn stderr $ show rs _ -> do hPutStrLn stderr "Unsupported file format." signFile :: Bool -> CommonArgsParsed -> [FilePath] -> String -> FilePath -> IO () signFile isHomeless cap keyrings keyid filename = do let mop = minimalOp isHomeless cap KikiResult r report <- runKeyRing mop { opFiles = opFiles mop `Map.union` Map.fromList [ (ArgFile f, strm { access = Sec }) | f <- keyrings ] } case r of KikiSuccess rt -> go rt err -> hPutStrLn stderr $ errorString err where go :: KeyRingRuntime -> IO () go rt = do tm <- modificationTime <$> getFileStatus filename bs <- L.readFile filename let hashed = [] -- TODO: FingerprintPacket unhashed = [IssuerPacket keyid] lit = LiteralDataPacket { format = 'b' -- b:binary, t:text, u:utf8 , filename = filename , timestamp = fromTime tm -- seconds since Jan 1, 1970 UTC , content = bs } hash = SHA512 case smallprGrip keyid of Nothing -> hPutStrLn stderr "Bad keygrip." Just grip -> do let keydata = lookupByGrip grip (rtKeyDB rt) case keydata of [] -> hPutStrLn stderr "No matching key." (k,_):_ -> rtPassphrases rt (Unencrypted,S2K 100 "") k >>= \case KikiSuccess un -> do mb <- pgpSign (Message [un]) (DataSignature lit []) hash keyid case mb of Nothing -> hPutStrLn stderr "Failed to make signature." Just o -> do putStrLn $ "Using "++show (fingerprint un)++" to write " <> filename <> ".sig" let sigs = map (\sig -> sig { unhashed_subpackets = unhashed }) (signatures_over o) L.writeFile (filename <> ".sig") $ L.concat $ map encode sigs err -> hPutStrLn stderr $ errorString err parsePackets :: L.ByteString -> Either String [Packet] parsePackets bs = case decodeOrFail bs of Left (more,off,er) -> Left er Right (more,off,pkt) -> do if (more/=L.empty) then parsePackets more >>= \pkts -> Right (pkt : pkts) else Right [pkt]