signify-hs (empty) → 0.1.0.1
raw patch · 6 files changed
+445/−0 lines, 6 filesdep +basedep +base64-bytestringdep +bytestringsetup-changed
Dependencies added: base, base64-bytestring, bytestring, crypto-api, cryptohash-sha512, cryptonite, eccrypto, filepath, optparse-applicative, parsec, signify-hs
Files
- CHANGELOG.md +5/−0
- LICENSE +30/−0
- Setup.hs +2/−0
- signify-hs.cabal +53/−0
- src/Crypto/ECC/Signify.hs +138/−0
- src/Main.hs +217/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for signify-hs++## 0.1.0.0 -- 2021-03-28++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 20[20..], Marcel Fourné++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Marcel Fourné nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ signify-hs.cabal view
@@ -0,0 +1,53 @@+name: signify-hs+version: 0.1.0.1+synopsis: A Haskell clone of OpenBSD signify.+description: This program with its corresponding library implements most of (OpenBSD) signify. Missing are GZip-header-embedding of signatures and checksum files.+license: BSD3+license-file: LICENSE+author: Marcel Fourné+maintainer: Marcel Fourné (haskell@marcelfourne.de)+-- copyright:+category: Cryptography+build-type: Simple+extra-source-files: CHANGELOG.md+cabal-version: >=1.10++library+ exposed-modules: Crypto.ECC.Signify+ -- other-modules:+ -- other-extensions:+ build-depends: base >=4.12 && < 5+ , bytestring+ , base64-bytestring+ , cryptohash-sha512+ , cryptonite+ , eccrypto+ , parsec+ hs-source-dirs: src+ default-language: Haskell2010+ ghc-options: -O2+ -Wall+ -Wincomplete-uni-patterns++executable signify-hs+ main-is: Main.hs+ other-modules: Crypto.ECC.Signify+ -- other-extensions:+ build-depends: base >=4.12 && < 5+ , bytestring+ , base64-bytestring+ , cryptohash-sha512+ , cryptonite+ , eccrypto+ , filepath+-- , MissingH+ , optparse-applicative+ , parsec+ , signify-hs+ if os(windows)+ build-depends: crypto-api >=0.13 && < 0.14+ hs-source-dirs: src+ default-language: Haskell2010+ ghc-options: -O2+ -Wall+ -Wincomplete-uni-patterns
+ src/Crypto/ECC/Signify.hs view
@@ -0,0 +1,138 @@+{-# LANGUAGE NoImplicitPrelude #-}++module Crypto.ECC.Signify ( parsePubKey+ , parseSignature+ , parseSecKey+ , printPubKey+ , printSignature+ , printSecKey+ ) where++import Text.Parsec+import Data.Either (Either(..))+import Data.String (String)+import Data.Function (($))+import Data.List ((++),drop,map)+import Data.Eq ((==),(/=))+import Data.Bool ((&&))+import Text.Show (show)+import Control.Monad+import qualified Data.ByteString as B+import Data.ByteString.Internal (c2w)+import Data.ByteString.Base64+import Crypto.ECC.Ed25519.Sign+import qualified Crypto.ECC.Ed25519.Internal.Ed25519 as DANGER+import Crypto.KDF.BCryptPBKDF+import Data.Bits+import qualified Crypto.Hash.SHA512 as H++type KeyID = B.ByteString+type Comment = String+type FileContent = B.ByteString+type Passphrase = B.ByteString+type Errormsg = String+type Salt = B.ByteString++parsePubKey :: FileContent -> Either Errormsg (Comment, KeyID, PubKey)+parsePubKey = parsePubOrSig++parseSignature :: FileContent -> Either Errormsg (Comment, KeyID, Signature)+parseSignature = parsePubOrSig++parseSecKey :: Passphrase -> FileContent -> Either Errormsg (Comment, KeyID, SecKey)+parseSecKey pass file = do+ (comment, rest) <- parseSignifyFileContent file+ let (kdfalg,rest2) = B.splitAt 2 rest+ (kdfrounds,rest3) = B.splitAt 4 rest2+ (salt,rest4) = B.splitAt 16 rest3+ (cksum,rest5) = B.splitAt 8 rest4+ (keyid,encbytes) = B.splitAt 8 rest5+-- rounds = fromBytes $ B.reverse kdfrounds+ rounds = 42 -- magic number, TODO: get rid of+ params = Parameters {iterCounts = rounds, outputLength = B.length encbytes}+ hashpw = generate params pass salt+ secbytes = B.pack $ B.zipWith xor encbytes hashpw+ resultbytes = if pass == B.empty then encbytes else secbytes+ if B.take 8 (H.hash resultbytes) == cksum+ then return (comment, keyid, DANGER.SecKeyBytes resultbytes)+ else Left "signify-hs: incorrect passphrase"++printPubKey :: KeyID -> PubKey -> Comment -> FileContent+printPubKey keyID pubKey comment = B.pack (map c2w ("untrusted comment: " ++ comment ++ " public key")) `B.append`+ B.pack (map c2w "\n") `B.append`+ encode (+ B.pack (map c2w "Ed") `B.append` -- signify file format magic+ keyID `B.append`+ pubKey+ ) `B.append`+ B.pack (map c2w "\n")++printSignature :: KeyID -> Signature -> Comment -> FileContent+printSignature keyID sig comment = B.pack (map c2w ("untrusted comment: " ++ comment)) `B.append`+ B.pack (map c2w "\n") `B.append`+ encode (+ B.pack (map c2w "Ed") `B.append` -- signify file format magic+ keyID `B.append`+ sig+ ) `B.append`+ B.pack (map c2w "\n")++printSecKey :: KeyID -> Passphrase -> Salt -> SecKey -> PubKey -> Comment -> FileContent+printSecKey keyID passphrase salt (DANGER.SecKeyBytes secKeyBytes) pubKeyBytes comment =+ let rounds = 42+ longkey = secKeyBytes `B.append` pubKeyBytes+ params = Parameters {iterCounts = rounds, outputLength = B.length longkey}+ hashpw = generate params passphrase salt+ secdata = B.pack $ B.zipWith xor longkey hashpw+ cksum = B.take 8 $ H.hash longkey+ fulldata = B.pack (map c2w "Ed") `B.append` -- signify file format magic+ B.pack (map c2w "BK") `B.append` -- signify file format magic+ B.pack (map c2w (if passphrase /= B.empty then "\NUL\NUL\NUL*" else "\NUL\NUL\NUL\NUL")) `B.append` -- manually hack rounds magic number 42 for now, TODO cleanly+ salt `B.append`+ cksum `B.append`+ keyID `B.append`+ (if passphrase /= B.empty then secdata else longkey)+ in B.pack (map c2w ("untrusted comment: " ++ comment ++ " secret key")) `B.append`+ B.pack (map c2w "\n") `B.append`+ encode fulldata `B.append`+ B.pack (map c2w "\n")++{-+-- Read bytes in big-endian order (most significant byte first)+-- Little-endian order is fromBytes . BS.reverse+fromBytes :: (Bits a, Num a) => B.ByteString -> a+fromBytes = B.foldl' f 0+ where+ f a b = a `shiftL` 8 .|. fromIntegral b++toBytes :: (Bits a, Num a) => a -> B.ByteString+toBytes = undefined+-- -}++parsePubOrSig :: FileContent -> Either Errormsg (Comment, KeyID, B.ByteString)+parsePubOrSig file = do+ (comment, rest) <- parseSignifyFileContent file+ let (keyid, signifydata) = B.splitAt 8 rest+ return (comment, keyid, signifydata)++parseSignifyFileContent :: FileContent -> Either Errormsg (Comment, B.ByteString)+parseSignifyFileContent file = do+ let res = parse signifyFile "(unknown)" file+ case res of+ Left s -> Left $ show s+ Right (comment,bytes) -> do+ let (alg,rest) = B.splitAt 2 bytes+ if alg /= B.pack (map c2w "Ed") && alg /= B.pack (map c2w "ED")+ then Left "currently unsupported signing algorithm"+ else return (drop 19 comment, rest)++signifyFile :: Parsec FileContent u (Comment, B.ByteString)+signifyFile = do+ comment <- many (noneOf "\r\n")+ _ <- endOfLine+ base64data <- many (noneOf "\r\n")+ _ <- endOfLine+ let base64decoded = decode $ B.pack $ map c2w base64data+ case base64decoded of+ Left s -> parserFail s+ Right dat -> return (comment,dat)
+ src/Main.hs view
@@ -0,0 +1,217 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE NoImplicitPrelude #-}++module Main where++import Options.Applicative as O+import Data.Maybe (fromMaybe,isNothing,Maybe(..))+import Data.Either (Either(..))+import Data.Bool (Bool(..),(||),otherwise)+import Data.String (String)+import Data.List ((++),drop)+import Data.Eq ((==))+import Data.Function (($))+import Crypto.ECC.Signify+import qualified Crypto.ECC.Ed25519.Sign as Ed+import qualified Data.ByteString as B+import Control.Monad+import Data.Semigroup ((<>))+import System.Exit+import System.Environment (getProgName)+import System.FilePath (takeBaseName)+import System.IO (hSetEcho,stdin,stdout,hFlush,IO(),FilePath,putStrLn,putStr,print)+-- import System.FileArchive.GZip++#ifndef mingw32_HOST_OS+import qualified Data.ByteString.Lazy.Char8 as BS8+#else+import qualified Crypto.Random as R+import Prelude (show)+#endif++data Opts = Opts+ {+ checksum :: Bool+ , generate :: Bool+ , sign :: Bool+ , verify :: Bool+ , pubkey :: Maybe FilePath+ , seckey :: Maybe FilePath+ , sigfile :: Maybe FilePath+ , message :: Maybe String+ , quiet :: Bool+ , comment :: String+ , embedmsg :: Bool+ , nopassphrase :: Bool+ , keytype :: Maybe String+ , gzipembed :: Bool+ , files :: [FilePath]+ }++options :: Parser Opts+options = Opts+ <$> switch+ (short 'C'+ <> help "Verify a signed checksum list, and then verify the checksum for each file." )+ <*> switch+ (short 'G'+ <> help "Generate a new key pair." )+ <*> switch+ (short 'S'+ <> help "Sign the specified message file and create a signature." )+ <*> switch+ (short 'V'+ <> help "Verify the message and signature match." )+ <*> optional (strOption+ (short 'p'+ <> help "Public key"+ <> metavar "pubkey"))+ <*> optional (strOption+ (short 's'+ <> help "Secret Key"+ <> metavar "seckey"))+ <*> optional (strOption+ (short 'x'+ <> help "Signature file to create or verify"+ <> metavar "sigfile"))+ <*> optional (strOption+ (short 'm'+ <> help "message"+ <> metavar "message"))+ <*> switch+ (short 'q'+ <> help "Whether to be quiet" )+ <*> strOption+ (short 'c'+ <> value "signify"+ <> help "comment"+ <> metavar "comment")+ <*> switch+ (short 'e'+ <> help "when signing, embed message after signature" )+ <*> switch+ (short 'n'+ <> help "don't force a passphrase" )+ <*> optional (strOption+ (short 't'+ <> help "keytypes???"+ <> metavar "keytype"))+ <*> switch+ (short 'z'+ <> help "sign and verify gzip archives, signing data embedded in the header" )+ <*> many (argument str (metavar "file ..."))++-- The horror of option combinations is contained here+main :: IO ()+-- main = do signify =<< customExecParser (prefs showHelpOnError) opts+main = signify =<< execParser opts+ where+ opts = info (options <**> helper)+ ( fullDesc+ <> progDesc "cryptographically sign and verify files"+ <> header "signify-hs, a Haskell clone of signify-openbsd" )++signify :: Opts -> IO ()+signify (Opts _ _ _ _ _ _ _ _ _ _ _ _ _ True _)+ = do putStrLn "gzip embedding not supported, yet"+ wrong+signify (Opts True _ _ _ _ _ Nothing _ q _ _ _ _ _ _)+ = do unless q $ putStrLn "must specify sigfile"+ wrong+signify (Opts True _ _ _ pub _ (Just sigfile') _ q _ _ _ _ _ files')+ = do putStrLn "Checksum file mode not supported, yet"+ wrong+signify (Opts _ True _ _ pub sec _ _ _ comment' _ nopass _ _ _)+ | isNothing pub || isNothing sec = putStrLn "must specify pubkey and seckey" >> wrong+ | otherwise = do+ passphrase <- if nopass then return B.empty else getcreatepassphrase+ keys <- Ed.genkeys+ case keys of+ Left e -> print e >> exitFailure+ Right (seck,pubk) ->+ do+#ifndef mingw32_HOST_OS+ bytes <- BS8.readFile "/dev/urandom"+ let salt = BS8.toStrict $ BS8.take 16 bytes+ keyID = BS8.toStrict $ BS8.take 8 $ BS8.drop 16 bytes+#else+ g <- R.getStdGen+ let bytes = R.randoms g+ salt = BS.pack $ BS8.take 16 bytes+ keyID = BS8.toStrict $ BS8.take 8 $ BS8.drop 16 bytes+#endif+ B.writeFile (fromMaybe "" pub) $ printPubKey keyID pubk comment'+ B.writeFile (fromMaybe "" sec) $ printSecKey keyID passphrase salt seck pubk comment'+signify (Opts _ _ True _ _ sec sig message' _ _ embed nopass _ gzipembed' _)+ | isNothing sec || isNothing message' = putStrLn "must specify message and seckey" >> wrong+signify (Opts _ _ True _ _ (Just secfile) sig (Just messagefile) _ _ embed nopass _ gzipembed' _) = do+ let targetfile = if isNothing sig then messagefile ++ ".sig" else let (Just sigfile') = sig in sigfile'+ passphrase <- if nopass then return B.empty else getpassphrase+ seccontent <- parseSecKey passphrase <$> B.readFile secfile+ case seccontent of+ Left e -> putStrLn e >> exitFailure+ Right (_, keyid, secKeyAndPubKey) -> do+ msgs <- B.readFile messagefile+ let comment' = "verify with " ++ takeBaseName secfile ++ ".pub"+ s = Ed.dsign secKeyAndPubKey msgs+ case s of+ Right sigs -> B.writeFile targetfile $ printSignature keyid sigs comment' `B.append` if embed then msgs else B.empty+ Left e -> putStrLn e >> exitFailure+signify (Opts _ _ _ True _ _ _ Nothing _ _ _ _ _ _ _)+ = putStrLn "must specify message" >> wrong+signify (Opts _ _ _ True pub _ sig (Just msgfile) q _ embed _ _ gzipembed' _)+ = do+ sigs <- case sig of+ Nothing -> parseSignature <$> B.readFile (msgfile ++ ".sig")+ Just sigfile' -> parseSignature <$> B.readFile sigfile'+ msgs <- B.readFile msgfile+ case sigs of+ Left _ -> wrong+ Right (comments, keyids, sig') -> do+ pubs <- case pub of+ Nothing -> let filep = drop 12 comments+ in parsePubKey <$> B.readFile ("/etc/signify/" ++ filep)+ Just pubfile -> parsePubKey <$> B.readFile pubfile+ case pubs of+ Left _ -> wrong+ Right (commentp, keyidp, pub') ->+ let x = Ed.dverify pub' sig' msgs+ in case x of+ Right Ed.SigOK -> unless q $ putStrLn "Signature Verified"+ _ -> do unless q $ putStrLn "signature verification failed"+ exitFailure+signify _ = wrong++wrong :: IO ()+wrong = printUsage >> exitFailure++printUsage :: IO ()+printUsage = do+ putStrLn "usage:\tsignify-hs -C [-q] [-p pubkey] [-t keytype] -x sigfile [file ...]"+ putStrLn "\tsignify-hs -G [-n] [-c comment] -p pubkey -s seckey"+ putStrLn "\tsignify-hs -S [-enz] [-x sigfile] -s seckey -m message"+ putStrLn "\tsignify-hs -V [-eqz] [-p pubkey] [-t keytype] [-x sigfile] -m message"+-- putStrLn "help:\tsignify-hs [-h|--help]"++getcreatepassphrase :: IO B.ByteString+getcreatepassphrase = do+ hSetEcho stdin False+ passphrase <- getpassphrase+ putStr "confirm passphrase: "+ hFlush stdout+ passphrase2 <- B.getLine+ putStrLn ""+ if passphrase == passphrase2+ then return passphrase+ else do name <- getProgName+ putStrLn $ name ++ ": passwords don't match"+ exitFailure++getpassphrase :: IO B.ByteString+getpassphrase = do+ hSetEcho stdin False+ putStr "passphrase: "+ hFlush stdout+ passphrase <- B.getLine+ putStrLn ""+ return passphrase