packages feed

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 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