{-# LANGUAGE DoAndIfThenElse #-} {-# LANGUAGE ViewPatterns #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE CPP #-} module Main ( main ) where import Control.Applicative import Control.Monad import Data.ASN1.BinaryEncoding import Data.ASN1.Encoding import Data.ASN1.Types import Data.Binary import Data.Bits import Data.Char import Data.IORef import Data.List import Data.Maybe import Data.OpenPGP import Data.Ord import Data.Text.Encoding import System.Environment import System.Exit import System.IO (hPutStrLn,stderr) import qualified Codec.Binary.Base64 as Base64 import qualified Crypto.Hash.RIPEMD160 as RIPEMD160 import qualified Crypto.Hash.SHA256 as SHA256 import qualified Data.ByteString as S import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy.Char8 as Char8 import qualified Data.Map as Map import qualified Data.Text as T import Control.Arrow (first,second) import Data.Binary.Get (runGet) import Data.Binary.Put (putWord32be,runPut,putByteString) import DotLock import LengthPrefixedBE import KeyRing import Base58 import qualified CryptoCoins import Data.OpenPGP.Util (verify,fingerprint) -- {-# ANN module ("HLint: ignore Eta reduce"::String) #-} -- {-# ANN module ("HLint: ignore Use camelCase"::String) #-} warn str = hPutStrLn stderr str sshrsa :: Integer -> Integer -> Char8.ByteString sshrsa e n = runPut $ do putWord32be 7 putByteString "ssh-rsa" put (LengthPrefixedBE e) put (LengthPrefixedBE n) decode_sshrsa :: Char8.ByteString -> Maybe RSAPublicKey decode_sshrsa bs = do let (pre,bs1) = Char8.splitAt 11 bs guard $ pre == runPut (putWord32be 7 >> putByteString "ssh-rsa") let rsakey = flip runGet bs1 $ do LengthPrefixedBE e <- get LengthPrefixedBE n <- get return $ RSAKey (MPI n) (MPI e) return rsakey isCertificationSig (CertificationSignature {}) = True isCertificationSig _ = True isSubkeySignature (SubkeySignature {}) = True isSubkeySignature _ = False verifyBindings keys nonkeys = (top ++ filter isSubkeySignature embedded,othersigs) where verified = do sig <- signatures (Message nonkeys) let v = verify (Message keys) sig guard (not . null $ signatures_over v) return v (top,othersigs) = partition isSubkeySignature verified embedded = do sub <- top let sigover = signatures_over sub unhashed = sigover >>= unhashed_subpackets subsigs = mapMaybe backsig unhashed -- This should consist only of 0x19 values -- subtypes = map signature_type subsigs -- trace ("subtypes = "++show subtypes) (return ()) -- trace ("issuers: "++show (map signature_issuer subsigs)) (return ()) sig <- signatures (Message ([topkey sub,subkey sub]++subsigs)) let v = verify (Message [subkey sub]) sig guard (not . null $ signatures_over v) return v smallpr k = drop 24 $ fingerprint k disjoint_fp ks = {- concatMap group2 $ -} transpose grouped where grouped = groupBy samepr . sortBy (comparing smallpr) $ ks samepr a b = smallpr a == smallpr b {- -- useful for testing group2 :: [a] -> [[a]] group2 (x:y:ys) = [x,y]:group2 ys group2 [x] = [[x]] group2 [] = [] -} getBindings :: [Packet] -> ( [([Packet],[SignatureOver])] -- ^ other signatures with key sets -- that were used for the verifications , [(Word8, (Packet, Packet), -- (topkey,subkey) [String], -- usage flags [SignatureSubpacket], -- hashed data [Packet])] -- ^ binding signatures ) getBindings pkts = (sigs,bindings) where (sigs,concat->bindings) = unzip $ do let (keys,_) = partition isKey pkts keys <- disjoint_fp keys let (bs,sigs) = verifyBindings keys pkts return . ((keys,sigs),) $ do b <- bs -- trace ("sigs = "++show (map (map signature_issuer . signatures_over) sigs)) bs i <- map signature_issuer (signatures_over b) i <- maybeToList i who <- maybeToList $ find_key fingerprint (Message keys) i let (code,claimants) = case () of _ | who == topkey b -> (1,[]) _ | who == subkey b -> (2,[]) _ -> (0,[who]) let hashed = signatures_over b >>= hashed_subpackets kind = guard (code==1) >> hashed >>= maybeToList . usage return (code,(topkey b,subkey b), kind, hashed,claimants) -- Returned data is simmilar to getBindings but the Word8 codes -- are ORed together. accBindings :: Bits t => [(t, (Packet, Packet), [a], [a1], [a2])] -> [(t, (Packet, Packet), [a], [a1], [a2])] accBindings bs = as where gs = groupBy samePair . sortBy (comparing bindingPair) $ bs as = map (foldl1 combine) gs bindingPair (_,p,_,_,_) = pub2 p where pub2 (a,b) = (pub a, pub b) pub a = fingerprint_material a samePair a b = bindingPair a == bindingPair b combine (ac,p,akind,ahashed,aclaimaints) (bc,_,bkind,bhashed,bclaimaints) = (ac .|. bc,p,akind++bkind,ahashed++bhashed,aclaimaints++bclaimaints) fpmatch grip key = (==) Nothing (fmap (backend (fingerprint key)) grip >>= guard . not) where backend xs ys = and $ zipWith (==) (reverse xs) (reverse ys) listKeys pkts = listKeysFiltered [] pkts listKeysFiltered grips pkts = do let (certs,bs) = getBindings pkts as = accBindings bs defaultkind (k:_) hs = k defaultkind [] hs = fromMaybe "subkey" ( listToMaybe . mapMaybe (fmap usageString . keyflags) $ hs) kinds = map (\(_,_,k,h,_)->defaultkind k h) as kindwidth = maximum $ map length kinds kindcol = min 20 kindwidth code (c,(m,s),_,_,_) = (fingerprint_material m,-c) ownerkey (_,(a,_),_,_,_) = a sameMaster (ownerkey->a) (ownerkey->b) = fingerprint_material a==fingerprint_material b matchgrip _ | null grips = True matchgrip ((code,(top,sub), kind, hashed,claimants):_) | any (flip fpmatch top . Just) grips = True matchgrip _ = False gs = filter matchgrip $ groupBy sameMaster (sortBy (comparing code) as) showsigs claimants = map (\k -> " " ++ "^ signed: " ++ fingerprint k) claimants subs@((_,(top,_),_,_,_):_) <- gs let subkeys = do (code,(top,sub), kind, hashed,claimants) <- subs let ar = case code of 0 -> " ??? " 1 -> " --> " 2 -> " <-- " 3 -> " <-> " formkind = take kindcol $ defaultkind kind hashed ++ repeat ' ' -- torhash = fromMaybe "" $ derToBase32 <$> derRSA sub (netid,kind') = maybe (0x0,"bitcoin") (\n->(CryptoCoins.publicByteFromName n,n)) $ listToMaybe kind unlines $ concat [ " " -- , grip top , ar , formkind , " " , fingerprint sub -- , " " ++ torhash -- , " " ++ (concatMap (printf "%02X") $ S.unpack (ecc_curve sub)) ] -- ++ ppShow hashed : if isCryptoCoinKey sub -- then (" " ++ "B⃦ " ++ bitcoinAddress sub) : showsigs claimants -- then (" " ++ "BTC " ++ bitcoinAddress sub) : showsigs claimants then (" " ++ "¢ "++kind'++":" ++ bitcoinAddress netid sub) : showsigs claimants else showsigs claimants torkeys = do (code,(top,sub), kind, hashed,claimants) <- subs guard ("tor" `elem` kind) guard (code .&. 0x2 /= 0) maybeToList $ derToBase32 <$> derRSA sub uid = {- fromMaybe "" . listToMaybe $ -} do (keys,sigs) <- certs sig <- sigs guard (isCertificationSig sig) guard (topkey sig == top) let issuers = do sig_over <- signatures_over sig i <- maybeToList $ signature_issuer sig_over maybeToList $ find_key (matchpr i) (Message keys) (reverse (take 16 (reverse i))) (primary,secondary) = partition (==top) issuers -- trace ("PRIMARY: "++show (map fingerprint primary)) $ return () -- trace ("SECONDARY: "++show (map fingerprint secondary)) $ return () guard (not (null primary)) let UserIDPacket uid = user_id sig parsed = parseUID uid ar = maybe " --> " (const " <-> ") $ do guard (uid_topdomain parsed == "onion" ) guard ( uid_realname parsed `elem` ["","Anonymous"]) guard ( uid_user parsed == "root" ) let subdom0 = L.fromChunks [encodeUtf8 (uid_subdomain parsed)] len = L.length subdom0 subdom = Char8.unpack subdom0 match = (==subdom) . take (fromIntegral len) guard (len >= 16) listToMaybe $ filter match torkeys unlines $ (" " ++ ar ++ "@" ++ " " ++ uid_full parsed) : showsigs secondary -- (_,sigs) = unzip certs "master-key " ++ fingerprint top ++ "\n" ++ uid ++" ...\n" ++ subkeys ++ "\n" {- - modify a UID to test the verify function properly - fails modifyUID (UserIDPacket str) = UserIDPacket str' where (fstname,rst) = break (==' ') str str' = mod fstname ++ rst mod "Bob" = "Bob Slacking" mod x = x modifyUID other = other -} readPublicKey :: Char8.ByteString -> RSAPublicKey readPublicKey bs = fromMaybe er $ do let (pre,bs1) = Char8.splitAt 7 bs guard $ pre == "ssh-rsa" let (sp,bs2) = Char8.span isSpace bs1 guard $ not (Char8.null sp) bs3 <- listToMaybe $ Char8.words bs2 qq <- L.pack `fmap` Base64.decode (Char8.unpack bs3) decode_sshrsa qq where er = error "Unsupported key format" -- | Returns the given list with its last element modified. toLast :: (x -> x) -> [x] -> [x] toLast f [] = [] toLast f [x] = [f x] toLast f (x:xs) = x : toLast f xs partitionStaticArguments specs args = psa args where smap = Map.fromList specs psa [] = ([],[]) psa (a:as) = case Map.lookup a smap of Nothing -> second (a:) $ psa as Just n -> first ((a:take n as):) $ psa (drop n as) show_wk secring_file grip db = do let sec_db = Map.filter gripmatch db gripmatch (KeyData p _ _ _) = Map.member secring_file (locations p) Message sec = flattenKeys False sec_db putStrLn $ listKeysFiltered (maybeToList grip) sec show_all db = do let Message packets = flattenKeys True db putStrLn $ listKeys packets show_whose_key input_key db = flip (maybe $ return ()) input_key $ \input_key -> do let ks = whoseKey input_key db case ks of [KeyData k _ uids _] -> do putStrLn $ fingerprint (packet k) mapM_ putStrLn $ Map.keys uids (_:_) -> error "ambiguous" [] -> return () show_pem keyspec wkgrip db = do let s = parseSpec wkgrip keyspec flip (maybe . void $ warn (keyspec ++ ": not found")) (selectPublicKey s db) $ \k -> do let rsa = pkcs8 . fromJust $ rsaKeyFromPacket k der = encodeASN1 DER (toASN1 rsa []) qq = Base64.encode (L.unpack der) putStrLn $ writePEM "PUBLIC KEY" qq -- ("TODO "++show keyspec) show_ssh keyspec wkgrip db = do let s = parseSpec wkgrip keyspec flip (maybe . void $ warn (keyspec ++ ": not found")) (selectPublicKey s db) $ \k -> do let Just (RSAKey (MPI n) (MPI e)) = rsaKeyFromPacket k bs = sshrsa e n blob = Base64.encode (L.unpack bs) putStrLn $ "ssh-rsa " ++ blob show_key keyspec wkgrip db = do let s = parseSpec "" keyspec let ps = do (_,k) <- filterMatches (fst s) (Map.toList db) mp <- flattenTop "" True k return $ packet mp -- putStrLn $ "show key " ++ show s putStrLn $ listKeys ps show_wip keyspec wkgrip db = do let s = parseSpec wkgrip keyspec flip (maybe $ void (warn (keyspec ++ ": not found"))) (selectSecretKey s db) $ \k -> do let nwb = maybe 0x80 CryptoCoins.secretByteFromName $ snd s putStrLn $ walletImportFormat nwb k cannonical_eckey x y = 0x4:pad32(numToBytes x) ++ pad32(numToBytes y) :: [Word8] where numToBytes n = reverse $ unfoldr getbyte n where getbyte d = do guard (d/=0) let (q,b) = d `divMod` 256 return (fromIntegral b,q) pad32 xs = replicate zlen 0 ++ xs where zlen = 32 - length xs bitcoinAddress network_id k = address where Just (MPI x) = lookup 'x' (key k) Just (MPI y) = lookup 'y' (key k) pub = cannonical_eckey x y hash = S.cons network_id . RIPEMD160.hash . SHA256.hash . S.pack $ pub address = base58_encode hash whoseKey :: RSAPublicKey -> KeyDB -> [KeyData] whoseKey rsakey db = filter matchkey (Map.elems db) where matchkey (KeyData k _ _ subs) = any (ismatch k) $ Map.elems subs ismatch k (SubKey mp sigs) = Just rsakey == rsaKeyFromPacket (packet mp) && any (check (packet k) (packet mp)) sigs check k sub (sig,_) = not . null $ do s <- signatures . Message $ [k,sub,packet sig] fw <- signatures_over $ verify (Message [k]) s subsig <- mapMaybe backsig (unhashed_subpackets $ packet sig) subsig_so <- signatures (Message [k,sub,subsig]) guard ( isSubkeySignature subsig_so && isSameKey (topkey subsig_so) k && isSameKey (subkey subsig_so) sub ) s2 <- signatures . Message $ [k,sub,subsig] signatures_over $ verify (Message [sub]) s2 isSameKey a b = sort (key apub) == sort (key bpub) where apub = secretToPublic a bpub = secretToPublic b kiki_usage = putStr . unlines $ {- ["kiki - a pgp key editing utility" ,"" ,"kiki [OPTIONS]" ,"" ," kiki merges a set of keyring files into a combined database and then" ," uses the database to update the files so that they have the most complete" ," information." ,"" ," The files pubring.gpg and subring.gpg in the directory specified by the " ," --homedir option are implicitly included in the keyring set." ,"" ," kiki can also import or export secret subkeys by using the --keypairs option." ,"" ," Subkeys that are imported with kiki are given an annotation \"usage@\" which" ," indicates what the key is for. This tag can be used as a SPEC to select a" ," particular key. Master keys may be specified by using fingerprints or by" ," specifying a substring of an associated UID." -} ["kiki sync [options...]" ,"" ,"Flags:" ," --homedir DIR" ," Where to find the the files secring.gpg and pubring.gpg. The " ," default location is taken from the environment variable " ," GNUPGHOME." ,"" ," --passphrase-fd N" ," Read passphrase from the given file descriptor." ,"" ," --import Add master keys to pubring.gpg. Without this option, only UID" ," and subkey data is updated. " ,"" ," --import-if-authentic" ," Add signed master keys to pubring.gpg. Like --import except that" ," only keys with signatures from the working key (--show-wk) are" ," imported." ,"" ," --autosign Sign all cross-certified tor-style UIDs." ," A tor-style UID is of the form:" ," Anonymous " ," It is considered cross certified if there exists a cross-certified" ," 'tor' subkey corresponding to the address HOSTNAME.onion." ,"" ,"Merging:" ," --keyrings FILE FILE..." ," Provide keyring files other than the implicit secring.gpg and" ," pubring.gpg in the --homedir. This option is implicit unless" ," --keypairs or --wallets is used." ,"" ," --wallets FILE FILE..." ," Provide wallet files with secret crypto-coin keys in Wallet" ," Import Format. The keys will be treated as subkeys of your" ," current working key (the one shown by --show-wk)." ,"" ," --keypairs KEYSPEC KEYSPEC..." ," Each KEYSPEC specifies that a key should match the content and" ," timestamp of an external PKCS #1 private RSA key file." ," " ," KEYSPEC ::= SPEC=FILE{CMD} " ,"" ," If neither SPEC or FILE match any keys, then the CMD will be " ," executed in order to create the FILE." ,"" ,"Output:" ," --show-wk Show fingerprints for the working key (which will be used to" ," make signatures) and all its subkeys and UID." ,"" ," --show-key SPEC" ," Show fingerprints for the specified key and all its subkeys" ," and UID." ,"" ," --show-all Show fingerprints and UIDs and usage tags for all known keys." ,"" ," --show-whose-key" ," Shows the fingerprint and UIDs of the key that owns the one that" ," is input on stdin in ssh-rsa format." ,"" ," --show-pem SPEC" ," Outputs the PKCS #8 public key corresponding to SPEC." ,"" ," --show-ssh SPEC" ," Outputs the ssh-rsa blob for the specified public key." ,"" ," --show-wip SPEC" ," Outputs the secret crypto-coin key in Wallet Input Format." ,"" ," --help Shows this help screen." ] doAutosign rt kd@(KeyData k ksigs umap submap) = ops where ops = map (\u -> InducerSignature u []) us us = filter torStyle $ Map.keys umap torStyle str = and [ uid_topdomain parsed == "onion" , uid_realname parsed `elem` ["","Anonymous"] , uid_user parsed == "root" , fmap (match . fst) (lookup (packet k) torbindings) == Just True ] where parsed = parseUID str match = (==subdom) . take (fromIntegral len) subdom0 = L.fromChunks [encodeUtf8 (uid_subdomain parsed)] subdom = Char8.unpack subdom0 len = T.length (uid_subdomain parsed) torbindings = getTorKeys (map packet $ flattenTop "" True kd) getTorKeys pub = do xs <- groupBindings pub (_,(top,sub),us,_,_) <- xs guard ("tor" `elem` us) let torhash = fromMaybe "" $ derToBase32 <$> derRSA sub return (top,(torhash,sub)) groupBindings pub = gs where (_,bindings) = getBindings pub bindings' = accBindings bindings code (c,(m,s),_,_,_) = (fingerprint_material m,-c) ownerkey (_,(a,_),_,_,_) = a sameMaster (ownerkey->a) (ownerkey->b) = fingerprint_material a==fingerprint_material b gs = groupBy sameMaster (sortBy (comparing code) bindings') kiki "sync" args_raw = do let (args,trail1) = break (=="--") args_raw trail = drop 1 trail1 (sargs,margs) = (sargs, foldl' (\m (k:xs)->Map.alter (appendArgs k xs) k m) Map.empty gargs) where (sargs,vargs) = partitionStaticArguments sargspec args sargspec = [ ("--homedir",1) , ("--passphrase-fd",1) , ("--import",0) , ("--autosign",0) , ("--import-if-authentic",0) , ("--show-wk",0) , ("--show-all",0) , ("--show-whose-key",0) , ("--show-key",1) , ("--show-pem",1) , ("--show-ssh",1) , ("--show-wip",1) , ("--help",0) ] argspec = map fst sargspec ++ ["--keyrings" ,"--keypairs" ,"--wallets" ,"--hosts"] -- "--bitcoin-keypairs" -- Disabled. We shouldn't accept private key -- data on the command line. args' = if map (take 1) (take 1 vargs) == ["-"] then vargs else "--keyrings":vargs gargs = (sargs ++) . toLast (++trail) . groupBy (\_ s-> take 1 s /= "-") $ args' appendArgs k xs opt = if k `elem` argspec then Just . maybe xs (++xs) $ opt else error . unlines $ [ "unrecognized option "++k , "Use --help for usage." ] -- putStrLn $ "margs = " ++ show (Map.assocs margs) unkeysRef <- newIORef Map.empty pwRef <- newIORef Nothing let keypairs0 = flip map (fromMaybe [] $ Map.lookup "--keypairs" margs) $ \specfile -> do let (spec,efilecmd) = break (=='=') specfile guard $ take 1 efilecmd=="=" let filecmd = drop 1 efilecmd let (file,bcmdb0) = break (=='{') filecmd bcmdb = if null bcmdb0 then "{}" else bcmdb0 guard $ take 1 bcmdb=="{" let bdmcb = (dropWhile isSpace . reverse) bcmdb guard $ take 1 bdmcb == "}" let cmd = (drop 1 . reverse . drop 1) bdmcb Just (spec,file,cmd) keyrings_ = fromMaybe [] $ Map.lookup "--keyrings" margs wallets = fromMaybe [] $ Map.lookup "--wallets" margs passphrase_fd = concat <$> Map.lookup "--passphrase-fd" margs when (any isNothing keypairs0) $ do warn "Syntax error in key pair specification" exitFailure input_key <- maybe (return Nothing) (const $ fmap (Just . readPublicKey) Char8.getContents) $ Map.lookup "--show-whose-key" margs let keypairs = catMaybes keypairs0 homespec = join . take 1 <$> Map.lookup "--homedir" margs passfd = fmap (FileDesc . read) passphrase_fd pems = flip map keypairs $ \(usage,path,cmd) -> let cmd' = guard (not $ null cmd) >> return cmd in (ArgFile path, (MutableRef cmd', PEMFile usage)) walts = map (\fname -> (ArgFile fname, (MutableRef Nothing, WalletFile))) wallets rings = map (\fname -> (ArgFile fname, (MutableRef Nothing, KeyRingFile passfd))) keyrings_ hosts = maybe [] (map decorate) $ Map.lookup "--hosts" margs where decorate fname = (ArgFile fname, (MutableRef Nothing, Hosts)) importStyle = maybe (\_ _ -> subkeysOnly) (\f rt kd -> f rt kd >> importPublic) $ mplus import_f importifauth_f where import_f = do Map.lookup "--import" margs return $ \rt kd -> Just () importifauth_f = do Map.lookup "--import-if-authentic" margs return guardAuthentic kikiOp = KeyRingOperation { kFiles = Map.fromList $ [ ( HomeSec, (MutableRef Nothing, KeyRingFile passfd) ) , ( HomePub, (MutableRef Nothing, KeyRingFile Nothing) ) ] ++ rings ++ pems ++ walts ++ hosts , kImports = Map.fromList [ ( HomePub, importStyle ) ] , kManip = maybe noManip (const doAutosign) $ Map.lookup "--autosign" margs , homeSpec = homespec } KikiResult rt report <- runKeyRing kikiOp case rt of KikiSuccess rt -> do -- interpret --show-* commands. let grip = rtGrip rt let shspec = Map.fromList [("--show-wk", const $ show_wk (rtSecring rt) grip) ,("--show-all",const show_all) ,("--show-whose-key", const $ show_whose_key input_key) ,("--show-key",\[x] -> show_key x $ fromMaybe "" grip) ,("--show-pem",\[x] -> show_pem x $ fromMaybe "" grip) ,("--show-ssh",\[x] -> show_ssh x $ fromMaybe "" grip) ,("--show-wip",\[x] -> show_wip x $ fromMaybe "" grip) ,("--help", \_ _ ->kiki_usage)] shargs = mapMaybe (\(x:xs) -> (,xs) <$> Map.lookup x shspec) sargs forM_ shargs $ \(cmd,args) -> cmd args (rtKeyDB rt) err -> putStrLn $ errorString err forM_ report $ \(fname,act) -> do putStrLn $ fname ++ ": " ++ reportString act kiki "working-key" args = do kiki "sync" ["--show-wk"] kiki "help" args = do putStrLn "Valid commands are:" let longest = maximum $ map (length . fst) commands pad cmd = take (longest+3) $ cmd ++ repeat ' ' forM commands $ \(cmd,help) -> do putStrLn $ " " ++ pad cmd ++ help return () commands :: [(String,String)] commands = [ ( "help", "display usage information" ) , ( "sync", "update key files of various kinds by propogating information" ) , ( "working-key", "show the current working master key and its subkeys" ) ] main = do dotlock_init args_raw <- getArgs case args_raw of [] -> kiki "working-key" [] cmd : args | cmd `elem` map fst commands -> kiki cmd args _ -> kiki "help" args_raw