{-# LANGUAGE OverloadedStrings #-} module Main where import Codec.Archive.Tar (Entry,EntryContent(..)) import qualified Codec.Archive.Tar as Tar import qualified Codec.Archive.Tar.Entry as Tar import qualified Codec.Compression.GZip as Gzip import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Lazy as L import qualified Data.Digest.Pure.MD5 as Md5 import Data.List import Data.Serialize import Data.String import System.Directory import System.Environment import System.FilePath import System.IO import System.Process data Options = OptionSign FilePath | Verify FilePath main = do (cmd:archive:_) <- getArgs case cmd of "sign" -> sumAndSign archive "verify" -> verify archive sumAndSign :: FilePath -> IO () sumAndSign fp = do exists <- doesFileExist fp if not exists then error $ fp ++ " doesn't exist" else do gzip <- L.readFile fp entries <- getGzipEntries gzip L.writeFile sum (checksum entries) rawSystem "gpg" ["--detach-sign",sum] removeFile sum addSignature fp (sum <.> "sig") entries where sum = translate ".sum" fp addSignature :: FilePath -> FilePath -> [Entry] -> IO () addSignature gz sig entries = do signature <- L.readFile sig case Tar.toTarPath False (makeSigName gz) of Left err -> error err Right spath -> do let sigEntry = Tar.fileEntry spath signature L.writeFile (translate ".signed.tar.gz" gz) (Gzip.compress (Tar.write (sigEntry : entries))) removeFile sig makeSigName = translate ".sig" . takeFileName translate ext = (++ ext) . dropSigned . dropExtension . dropExtension where dropSigned x | isSuffixOf ".signed" x = dropExtension x | otherwise = x getGzipEntries gzip = case result of Left err -> error ("tar reading error: " ++ show err) Right entries -> return entries where result = Tar.foldEntries (fmap . (:)) (Right []) Left (Tar.read (Gzip.decompress gzip)) checksum :: [Entry] -> ByteString checksum entries = L.intercalate "\n" (filter (not . L.null) (map hashEntry (sort (map Tar.entryContent entries)))) hashEntry :: EntryContent -> ByteString hashEntry entry = case entry of NormalFile bytes _ -> hexify (Md5.md5 bytes) SymbolicLink target -> encodeTarget target HardLink target -> encodeTarget target OtherEntryType typ bytes _ -> hexify (Md5.md5 (L.cons (word typ) bytes)) CharacterDevice major minor -> L.cons (word 'c') (L.concat [iword major,iword minor]) BlockDevice major minor -> L.cons (word 'b') (L.concat [iword major,iword minor]) _ -> L.empty where encodeTarget = fromString . Tar.fromLinkTarget iword = fromString . show hexify = fromString . show word = fromIntegral . fromEnum verify :: FilePath -> IO () verify fp = do exists <- doesFileExist fp if not exists then error $ fp ++ " doesn't exist" else do gzip <- L.readFile fp entries <- getGzipEntries gzip case find isSig entries of Nothing -> error $ "unable to find " ++ sigName ++ " in archive" Just entry -> do L.writeFile sum (checksum (filter (not . isSig) entries)) L.writeFile sig (getEntryFileContent entry) rawSystem "gpg" ["--verify",sig,sum] removeFile sum removeFile sig where sigName = makeSigName fp sum = translate ".sum" fp isSig = (==sigName) . Tar.entryPath sig = translate ".sig" fp getEntryFileContent entry = case Tar.entryContent entry of NormalFile bytes _ -> bytes _ -> error "malformed signature in the tar archive"