diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for signify-hs
+
+## 0.1.0.0 -- 2021-03-28
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/signify-hs.cabal b/signify-hs.cabal
new file mode 100644
--- /dev/null
+++ b/signify-hs.cabal
@@ -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
diff --git a/src/Crypto/ECC/Signify.hs b/src/Crypto/ECC/Signify.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/ECC/Signify.hs
@@ -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)
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -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
