packages feed

ssh 0.2.13.1 → 0.3

raw patch · 23 files changed

+762/−59 lines, 23 filesdep +HUnitdep +QuickCheckdep +directorydep ~basedep ~bytestringnew-uploader

Dependencies added: HUnit, QuickCheck, directory, filepath, integer-gmp, libssh2, pseudomacros, ssh, tasty, tasty-hunit, tasty-quickcheck, template-haskell, th-lift-instances

Dependency ranges changed: base, bytestring

Files

src/SSH.hs view
@@ -2,7 +2,8 @@  import Control.Concurrent (forkIO) import Control.Concurrent.Chan-import Control.Monad (replicateM, when)+import Control.Exception (bracket)+import Control.Monad (replicateM) import Control.Monad.Trans.State import Data.Digest.Pure.SHA (bytestringDigest, sha1) import Crypto.HMAC@@ -10,7 +11,7 @@ import Data.List (intercalate) import Data.List.Split (splitOn) import Network-import OpenSSL.BN (randIntegerOneToNMinusOne)+import OpenSSL.BN (randIntegerOneToNMinusOne, modexp) import System.IO import System.Random import qualified Data.ByteString.Lazy as LBS@@ -65,12 +66,33 @@ supportedLanguages :: String supportedLanguages = "" +data Config =+    Config {+        cSession     :: SessionConfig,+        cChannel     :: ChannelConfig,+        cPort        :: PortNumber,+        cReadyAction :: IO ()+    }+++startedMessage :: PortNumber -> IO ()+startedMessage p = putStrLn $ "ssh server listening on port " ++ show p+ start :: SessionConfig -> ChannelConfig -> PortNumber -> IO ()-start sc cc p = withSocketsDo $ do-    sock <- listenOn (PortNumber p)-    putStrLn $ "ssh server listening on port " ++ show p-    waitLoop sc cc sock+start sc cc p = startConfig (Config sc cc p (startedMessage p)) +startConfig :: Config -> IO ()+startConfig config = withSocketsDo $ do+    -- waitLoop never actually exits so we could just use finally,+    -- but bracket seems more future proof+    bracket+       (listenOn (PortNumber (cPort config)))+       sClose+       (\sock -> do+           cReadyAction config+           waitLoop (cSession config) (cChannel config) sock+       )+ waitLoop :: SessionConfig -> ChannelConfig -> Socket -> IO () waitLoop sc cc s = do     (handle, hostName, port) <- accept s@@ -232,9 +254,10 @@  kexDHInit :: Session () kexDHInit = do-    e <- net readInteger+    e <- net readInteger -- other party's public number     dump ("KEXDH_INIT", e) +   -- our private number     y <- io $ randIntegerOneToNMinusOne ((safePrime - 1) `div` 2) -- q?      let f = modexp generator y safePrime@@ -335,36 +358,54 @@     authMethods <- gets (scAuthMethods . ssConfig)      dump ("userauth attempt", user, service, method)-    check <- case fromLBS method of-        x | not (x `elem` authMethods) ->-            return False +    let+        authorized = do+            sendPacket userAuthOK+            modify (\s -> s { ssUser = Just (fromLBS user) })++        authfailed = sendPacket $ userAuthFail authMethods++    case fromLBS method of+        x | not (x `elem` authMethods) -> authfailed+         "publickey" -> do             b <- net readByte             name <- net readLBS             key <- net readLBS-            ch <- auth (PublicKey (fromLBS user) (blobToKey key)) -            -- if it's signed, assume it's the second one after auth-            if ch && b == 1-                then sendPacket userAuthOK-                else when ch (sendPacket $ userAuthPKOK name key)+            let pkey = blobToKey key+            ch <- auth (PublicKey (fromLBS user) pkey) -            return ch+            case (ch, b == 1) of+              (False, _) -> authfailed+              (True, True) ->+                  do sig <- net readLBS+                     sessionID <- gets ssID+                     let message =+                           doPacket $ do+                               byteString sessionID+                               byte 50 -- SSH_MSG_USERAUTH_REQUEST+                               byteString user+                               byteString service+                               string "publickey"+                               byte 1 -- TRUE+                               byteString name+                               byteString key+                     ok <- io $ verify pkey message sig+                     if ok then authorized else authfailed+              (True, False) -> sendPacket $ userAuthPKOK name key          "password" -> do             0 <- net readByte             password <- net readLBS             ch <- auth (Password (fromLBS user) (fromLBS password))-            when ch (sendPacket userAuthOK)-            return ch+            if ch then authorized else authfailed          u -> error $ "unhandled authorization type: " ++ u -    if check-        then modify (\s -> s { ssUser = Just (fromLBS user) })-        else sendPacket (userAuthFail authMethods)   where+     userAuthFail ms = do         byte 51         string (intercalate "," ms)
src/SSH/Crypto.hs view
@@ -2,8 +2,8 @@  import Control.Monad (replicateM) import Control.Monad.Trans.State-import Data.ASN1.BinaryEncoding (BER(..))-import Data.ASN1.Encoding (decodeASN1)+import Data.ASN1.BinaryEncoding (BER(..), DER(..))+import Data.ASN1.Encoding (decodeASN1, encodeASN1) import Data.ASN1.Stream import Data.Digest.Pure.SHA (bytestringDigest, sha1) import Data.List (isPrefixOf)@@ -52,6 +52,11 @@     = RSAKeyPair         { rprivPub :: PublicKey         , rprivD :: Integer+        , rprivPrime1 :: Integer+        , rprivPrime2 :: Integer+        , rprivExponent1 :: Integer+        , rprivExponent2 :: Integer+        , rprivCoefficient :: Integer         }     | DSAKeyPair         { dprivPub :: PublicKey@@ -59,28 +64,59 @@         }     deriving (Eq, Show) -+-- for API compatibility rsaKeyPairFromFile :: FilePath -> IO KeyPair-rsaKeyPairFromFile fn = do+rsaKeyPairFromFile = keyPairFromFile++keyPairFromFile :: FilePath -> IO KeyPair+keyPairFromFile fn = do     x <- readFile fn-    let asn1-            = B64.decode-            . concat-            . filter (not . ("--" `isPrefixOf`))-            . lines-            $ x+    return $ parseKeyPair x -    case decodeASN1 BER (toLBS asn1) of+removeKeyPairHeaderFooter :: [String] -> (String, [String])+removeKeyPairHeaderFooter xs =+   (reverse . drop 17 . reverse . drop 11 . head $ xs, filter (not . ("--" `isPrefixOf`)) xs)++addKeyPairHeaderFooter :: String -> [String] -> [String]+addKeyPairHeaderFooter what xs =+   ["-----BEGIN " ++ what ++ " PRIVATE KEY-----"] ++ xs ++ ["-----END " ++ what ++ " PRIVATE KEY-----"]++-- |Parse an key pair from OpenSSH private key file format.+parseKeyPair :: String -> KeyPair+parseKeyPair x =+    let (what, body) = removeKeyPairHeaderFooter . lines $ x++        asn1 = B64.decode . concat $ body++    in case decodeASN1 BER (toLBS asn1) of         Right (Start Sequence:ss)             | all isIntVal (fst $ getConstructedEnd 0 ss) ->             let (is, _) = getConstructedEnd 0 ss-            in return $ RSAKeyPair-                { rprivPub = RSAPublicKey-                    { rpubE = intValAt 2 is-                    , rpubN = intValAt 1 is+            in case what of+                 "RSA" ->+                   RSAKeyPair+                     { rprivPub = RSAPublicKey+                        { rpubE = intValAt 2 is+                        , rpubN = intValAt 1 is+                        }+                    , rprivD = intValAt 3 is+                    , rprivPrime1 = intValAt 4 is+                    , rprivPrime2 = intValAt 5 is+                    , rprivExponent1 = intValAt 6 is+                    , rprivExponent2 = intValAt 7 is+                    , rprivCoefficient = intValAt 8 is                     }-                , rprivD = intValAt 3 is-                }+                 "DSA" ->+                   DSAKeyPair+                    { dprivPub = DSAPublicKey+                       { dpubP = intValAt 1 is+                       , dpubQ = intValAt 2 is+                       , dpubG = intValAt 3 is+                       , dpubY = intValAt 4 is+                       }+                    , dprivX = intValAt 5 is+                    }+                 _ -> error ("unknown key type: " ++ what)         Right u -> error ("unknown ASN1 decoding result: " ++ show u)         Left e -> error ("ASN1 decoding of private key failed: " ++ show e)   where@@ -90,8 +126,34 @@     intValAt i is =         case is !! i of             IntVal n -> n-            x -> error ("not an IntVal: " ++ show x)+            v -> error ("not an IntVal: " ++ show v) +-- |Turn an key pair into OpenSSH private key file format.+printKeyPair :: KeyPair -> String+printKeyPair keyPair =+  unlines . addKeyPairHeaderFooter what . lines . B64.encode . fromLBS . encodeASN1 DER $ asn1Structure++    where++      (what, asn1Structure) =+        case keyPair of+          (RSAKeyPair { rprivPub = RSAPublicKey { rpubE = e, rpubN = n },+                        rprivD = d, rprivPrime1 = p1, rprivPrime2 = p2,+                        rprivExponent1 = exp1, rprivExponent2 = exp2, rprivCoefficient = c+                      })+             -> ("RSA", [Start Sequence, IntVal 0, IntVal n, IntVal e, IntVal d,+                         IntVal p1, IntVal p2, IntVal exp1, IntVal exp2, IntVal c, End Sequence])++          (DSAKeyPair { dprivPub = DSAPublicKey { dpubP = p, dpubQ = q, dpubG = g, dpubY = y }, dprivX = x })+             -> ("DSA", [Start Sequence, IntVal 0, IntVal p, IntVal q, IntVal g,+                         IntVal y, IntVal x, End Sequence])++          _ -> error "printKeyPair: unsupported key pair"++++-- these are the generator and prime for the "Second Oakley Group" described in RFC 2409+ generator :: Integer generator = 2 @@ -108,13 +170,12 @@ fromBlocks :: [LBS.ByteString] -> LBS.ByteString fromBlocks = LBS.concat -modexp :: Integer -> Integer -> Integer -> Integer-modexp = modexp' 1-  where-    modexp' y _ 0 _ = y-    modexp' y z e n-        | e `mod` 2 == 1 = modexp' (y * z `mod` n) ((z ^ (2 :: Integer)) `mod` n) (e `div` 2) n-        | otherwise = modexp' y ((z ^ (2 :: Integer)) `mod` n) (e `div` 2) n+rsaKeyLen :: PublicKey -> Int+-- There's no explicit indication of size in the key format so we just+-- have to look at the magnitude of the numbers.+-- This is consistent with what e.g. openssh does.+rsaKeyLen (RSAPublicKey _e n) = (1 + integerLog2 n) `div` 8+rsaKeyLen _ = error "rsaKeyLen: not an RSA public key"  blob :: PublicKey -> LBS.ByteString blob (RSAPublicKey e n) = doPacket $ do@@ -143,9 +204,11 @@         u -> error $ "unknown public key format: " ++ u  sign :: KeyPair -> LBS.ByteString -> IO LBS.ByteString-sign (RSAKeyPair (RSAPublicKey e n) d) m = return $ LBS.concat+sign (RSAKeyPair p@(RSAPublicKey e n) d _ _ _ _ _) m = do+  let keyLen = rsaKeyLen p+  return $ LBS.concat     [ netString "ssh-rsa"-    , netLBS (RSA.rsassa_pkcs1_v1_5_sign RSA.ha_SHA1 (RSAKey.PrivateKey (RSAKey.PublicKey 256 n e) d 0 0 0 0 0) m)+    , netLBS (RSA.rsassa_pkcs1_v1_5_sign RSA.ha_SHA1 (RSAKey.PrivateKey (RSAKey.PublicKey keyLen n e) d 0 0 0 0 0) m)     ] sign (DSAKeyPair (DSAPublicKey p q g y) x) m = do     (r, s) <- DSA.signDigestedDataWithDSA (DSA.tupleToDSAKeyPair (p, q, g, y, x)) digest@@ -159,3 +222,24 @@   where     digest = strictLBS . bytestringDigest . sha1 $ m sign _ _ = error "sign: invalid key pair"++-- |The length of the actual signature for a given key+-- The actual signature data is always found at the end of a complete signature,+-- so can be extracted by just grabbing this many bytes at the end.+actualSignatureLength :: PublicKey -> Int+actualSignatureLength p@(RSAPublicKey {}) = rsaKeyLen p+actualSignatureLength (DSAPublicKey {}) = 40++verify :: PublicKey -> LBS.ByteString -> LBS.ByteString -> IO Bool+verify p@(RSAPublicKey e n) message signature = do+    let keyLen = rsaKeyLen p+        realSignature = LBS.drop (LBS.length signature - fromIntegral keyLen) signature+    return $ RSA.rsassa_pkcs1_v1_5_verify RSA.ha_SHA1 (RSAKey.PublicKey keyLen n e) message realSignature++verify (DSAPublicKey p q g y) message signature = do+    let realSignature = LBS.drop (LBS.length signature - 40) signature+        r = fromOctets (256 :: Integer) (LBS.unpack (LBS.take 20 realSignature))+        s = fromOctets (256 :: Integer) (LBS.unpack (LBS.take 20 (LBS.drop 20 realSignature)))+    DSA.verifyDigestedDataWithDSA (DSA.tupleToDSAPubKey (p, q, g, y)) digest (r, s)+  where+    digest = strictLBS . bytestringDigest . sha1 $ message
src/SSH/Session.hs view
@@ -13,7 +13,7 @@ import qualified Data.Map as M  import SSH.Channel-import SSH.Crypto+import SSH.Crypto hiding (verify) import SSH.Debug import SSH.NetReader import SSH.Packet@@ -86,7 +86,7 @@     SessionConfig         { scAuthMethods = ["publickey"]         , scAuthorize = const (return True)-        , scKeyPair = RSAKeyPair (RSAPublicKey 0 0) 0+        , scKeyPair = RSAKeyPair (RSAPublicKey 0 0) 0 0 0 0 0 0         {-\(Password u p) ->-}             {-return $ u == "test" && p == "test"-}         }
src/SSH/Util.hs view
@@ -1,10 +1,14 @@+{-# LANGUAGE MagicHash #-} module SSH.Util where  import Data.Word (Word8) import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS +import GHC.Base (Int(I#))+import GHC.Integer.Logarithms (integerLog2#) + toLBS :: String -> LBS.ByteString toLBS = LBS.pack . map (fromIntegral . fromEnum) @@ -41,3 +45,7 @@          pad = replicate (l - unPaddedLen) (0x00::Word8) 	 z = toOctets (256 :: Integer) y 	 unPaddedLen = length z++integerLog2 :: Integer -> Int+integerLog2 n | n <=0 = error "integerLog2: argument must be positive"+integerLog2 n = I# (integerLog2# n)
ssh.cabal view
@@ -1,30 +1,45 @@ name:                ssh-version:             0.2.13.1+version:             0.3 synopsis:            A pure-Haskell SSH server library. description:     This a library for implementing your own servers that handle SSH     requests and authorization, etc. Similar to Python's Twisted Conch     library. It's used eg by darcsden to provide basic SSH access. -    This package receives only basic maintenance; if you'd like to-    take it over, please contact the current maintainer. -homepage:            http://hub.darcs.net/simon/ssh+homepage:            http://hub.darcs.net/ganesh/ssh license:             BSD3 license-file:        LICENSE author:              Alex Suraci <suraci.alex@gmail.com>-maintainer:          Simon Michael <simon@joyful.com>+maintainer:          Ganesh Sittampalam <ganesh@earth.li> category:            Network build-type:          Simple-cabal-version:       >= 1.6+cabal-version:       >= 1.8 stability:           Unstable tested-with:         GHC==7.8.2-data-files:          CHANGES,-                     README+extra-source-files:  CHANGES,+                     README,+                     test/keys/host,+                     test/keys/host.pub,+                     test/keys/client/id_rsa_test,+                     test/keys/client/id_rsa_test.pub,+                     test/keys/client/id_rsa_test2,+                     test/keys/client/id_rsa_test2.pub,+                     test/keys/client/id_rsa_1024,+                     test/keys/client/id_rsa_1024.pub,+                     test/keys/client/id_rsa_2048,+                     test/keys/client/id_rsa_2048.pub,+                     test/keys/client/id_rsa_4096,+                     test/keys/client/id_rsa_4096.pub,+                     test/keys/client/id_dsa,+                     test/keys/client/id_dsa.pub,+                     test/keys/client/id_dsa2,+                     test/keys/client/id_dsa2.pub + source-repository   head     type:           darcs-    location:       http://hub.darcs.net/simon/ssh+    location:       http://hub.darcs.net/ganesh/ssh  library   hs-source-dirs:   src@@ -52,6 +67,7 @@                     crypto-pubkey-types >= 0.2,                     cryptohash-cryptoapi,                     HsOpenSSL >= 0.8,+                    integer-gmp >= 0.5 && < 0.6,                     network,                     process,                     RSA >= 1.2 && < 1.3,@@ -60,3 +76,28 @@                     SimpleAES,                     split,                     transformers++test-suite ssh-test+  type: exitcode-stdio-1.0+  hs-source-dirs: test+  main-is:        test.hs+  other-modules:    EmbedTree++  ghc-options: -Wall -threaded++  build-depends:+      tasty                      >= 0.10  && < 0.11,+      tasty-hunit                >= 0.9   && < 0.10,+      tasty-quickcheck           >= 0.8   && < 0.9,+      HUnit                      >= 1.0   && < 1.3,+      QuickCheck                 >= 2.8   && < 2.9,+      libssh2                    >= 0.2   && < 0.3,+      filepath                   >= 1.3   && < 1.4,+      directory                  >= 1.2   && < 1.3,+      bytestring                 >= 0.10  && < 0.11,+      template-haskell           >= 2.8   && < 2.10,+      th-lift-instances          >= 0.1   && < 0.2,+      pseudomacros               >= 0.0   && < 0.1,+      containers,+      base,+      ssh
+ test/EmbedTree.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE TemplateHaskell, TupleSections #-}+module EmbedTree where++import Control.Applicative ((<$>))+import qualified Data.Map as Map+import Data.Map (Map)++import Instances.TH.Lift ()++import Language.Haskell.TH+import Language.Haskell.TH.Syntax++import PseudoMacros++import System.Directory+  (doesFileExist, doesDirectoryExist+  ,getDirectoryContents+  )+import System.FilePath (takeDirectory, (</>))++data Entry = File String | Directory (Map String Entry)++getFile :: Entry -> String+getFile (File contents) = contents+getFile (Directory _) = error $ "getFile: found a directory"++getDirectory :: Entry -> Map String Entry+getDirectory (File _) = error $ "getDirectory: found a file"+getDirectory (Directory contents) = contents++getEntry :: String -> Map String Entry -> Entry+getEntry name entries =+  case Map.lookup name entries of+    Nothing -> error $ "getEntry: " ++ name ++ " not found"+    Just v -> v++instance Lift Entry where+  lift (File contents) = AppE (ConE 'File) <$> [|contents|]+  lift (Directory entries) = AppE (ConE 'Directory) <$> [|entries|]++readTree :: FilePath -> IO Entry+readTree path = do+  isFile <- doesFileExist path+  if isFile+    then File <$> readFile path+    else do+      isDirectory <- doesDirectoryExist path+      if isDirectory+        then do+          entries <- filter (`notElem` [".", ".."]) <$> getDirectoryContents path+          Directory . Map.fromList <$> mapM (\entry -> (entry,) <$> readTree (path </> entry)) entries+        else fail $ path ++ " is not a file or directory"++embedTree :: FilePath -> ExpQ+embedTree relPath = do+    t <- runIO (readTree (takeDirectory $__FILE__ </> relPath))+    [|t|]
+ test/keys/client/id_dsa view
@@ -0,0 +1,12 @@+-----BEGIN DSA PRIVATE KEY-----+MIIBuwIBAAKBgQDOY5c/pQcpuFrzVB4OPxFsp/UDRUu5IwUncOr0Yr39n+Y8Jjy2+l/lLoSrZiYO2An1xuTESkUmyw3AkTRGy2YXFgCbD/rQ7/SSFLtYnQWk3DXqNGIPe+XeEQatSEB2VjFwGxkpL+TZ+oZCYyLTa535QYRmBu5OTikiJwlSicsHMmlwIVALT6+OZGvc3s2fmGss6CNfnIB89LlAoGAbni81v5fJATvHhHikmw2y2C/10m24rq1uH9F+XYjXMwoIOgB4/E2375CvMMsCHMF3g07vsetgOd93E3XHDFf7ZsKX8fPNN6BtMJgS+Q5gRKHq8c1FmYKkhOwjKSrh1T10sbp6fvhs8hjh7xxv+GabMltnliowRJgP4z1/Z+fEkg78oCgYEAt3f+TU+z/Ma7IgxU32Jd5BhBNg6wINctWs4ecaXg72F4CxlZSh6/+a0SQeThYMaqYsGsOpAN3iJFiZ1F+GJZ/WrJ2Y/J4r09InawAW0ISHk7LZ9PmI9wU+G6K63WNymNuOljZQPyfyd/hi0M2LqzBlEOsvYrRHZbIa6vcRv5SqjNYCFDxx6Mmt+Y43fIInyCuhiek6d3gHl+-----END DSA PRIVATE KEY-----
+ test/keys/client/id_dsa.pub view
@@ -0,0 +1,1 @@+ssh-dss AAAAB3NzaC1kc3MAAACBAM5jlz+lBym4WvNUHg4/EWyn9QNFS7kjBSdw6vRivf2f5jwmPLaX+UuhKtmJg7YCfXG5MRKRSbLDcCRNEbLZhcWAJsP+tDv9JIUu1idBaTcNeo0Yg95d4RBq1IQHZWMXAbGSkv5Nn6hkJjItNrnflBhGYG7k5OKSInCVKJywcyaXAAAAFQC0+jmRr3N7Nn5hrLOgjX5yAfPS5QAAAIBueLzW/l8kBO8eEeKSbDbLYL/XSbbiurW4f0VdiNczCgg6AHj8TbfvkK8wywIcwXeDTu+x62A533cTdccMV/tmwpfx8803oG0wmBJDmBEoerxzUWZgqSE7CMpKuHVPXSxunp++GzyGOHvHG/4ZpsyW2eWKjBEmA/jPX9l8SSDvygAAAIEAt3f+TU+z/Ma7IgxU32Jd5BhBNg6wINctWs4ecaXg72F4CxlZSh6/a0SQeThYMaqYsGsOpAN3iJFiZ1F+GJZ/WrJ2Y/J4r09InawAW0ISHk7LZ9PmI9wUG6K63WNymNuOljZQPyfyd/hi0M2LqzBlEOsvYrRHZbIa6vcRv5SqjNY= ganesh@paxton
+ test/keys/client/id_dsa2 view
@@ -0,0 +1,12 @@+-----BEGIN DSA PRIVATE KEY-----+MIIBuwIBAAKBgQD3ncQcimGZGV5mMZxfYRpfMcwUhMXbW7jUuWLDvfMXm9dpfw4H+nKeXP7LtDSjzUQreXA6lf20wh3/dxsKNFAbqPwgDuOxZD5JXPahMaoNqOg+/V5DD+qhTjJrxGzsMCWstYV1nsCQMN4aVlZLqJ4dhiVwwRIkCbPXFmhuJYxhL8aQIVAO9L+HPk1IGpMSi2gcGqKS754Kr+JAoGAfWTPY44qm5T+z6Tn7EwVaak5wj4XwrzvfGKY+ay/LKyPGn9m8Y+YfJ2zv1QQyTXmNC1wjpKI8ZL6ff8d0ft1RO6+HsFvoCSHDWPKR+x5ZopfUg/pDeiVwwU83IhSmPUECz/+8jGZ+rKrQdLc3bc1GhyfvcsAWL5PBwzqDs+z4X4WTUCgYAPrLJresqVe+jDHRIzvW5WhyYgyjN2PMHYxV/J7i54pM4Qj/zHPiK3+TpeuaCLgih2DQbtJrn+4KfIbv+/v2AsHGcj/kZEGvS1+t+pX98zo10gdsSAVoOig+nAp4Rt18vFbSc+0xQgj0NFrWvSrU6bszSVD1fYGkpdzbg+WSwscMsQIVALC8wPU1+fTf6hMJojbMUTbOZWMRM+-----END DSA PRIVATE KEY-----
+ test/keys/client/id_dsa2.pub view
@@ -0,0 +1,1 @@+ssh-dss AAAAB3NzaC1kc3MAAACBAPedxByKYZkZXmYxnF9hGl8xzBSExdtbuNS5YsO98xeb12l/Dgecp5c/su0NKPNRCt5cDqV/bTCHf93Gwo0UBuo/CAO47FkPklc9qExqg2o6D79XkMOqFOMmvEbOwwJay1hXWewJAw3hpWVkuonh2GJXDBEiQJs9cWaG4ljGEvxpAAAAFQDvSxz5NSBqTEotoHBqiku+eCq/iQAAAIB9ZM9jjiqblP7PpOfsTBVpqTnCPhfCvO98YphrL8srI8af2bxj5h8nbO/VBDJNeY0LXCOkojxkvp9/x3R+3VE7r4ewW+gJIcNY8pHHlmil9SD+kN6JXDBTzciFKY9QQLP/7yMZn6sqtB0tzdtzUaHJ+9ywBYvk8HDOoOzPhfhZNQAAAIAPrLJresqVe+jDHRIzvW5WhyYgyjN2PMHYxV/J7i54pM4Qj/zHPiK3TpeuaCLgih2DQbtJrn+4KfIbv+/v2AsHGcj/kZEGvS1+t+pX98zo10gdsSAVoOignAp4Rt18vFbSc+0xQgj0NFrWvSrU6bszSVD1fYGkpdzbg+WSwscMsQ== ganesh@paxton
+ test/keys/client/id_rsa_1024 view
@@ -0,0 +1,15 @@+-----BEGIN RSA PRIVATE KEY-----+MIICXAIBAAKBgQCVinDqmlOiQg+qIGP//qby2v9huGNp2z0pWngHGD1qdZtq5Mkj+PgYEbWO1xm0CIE2dLKwb704+U6Cj0ZM7vi1PwteojKG3Opt4O9vfdLpM6LSkIUPi+jGvIH0sbSwSTtOv2DPD60bvyeqwLDpfh+zZgLXC7dbi72W9IsN86k8h38QIDAQAB+AoGANG0UqSrxpzEBzOMOK/FbFkkwv1GliLcT+it9aP9WiLwygIc0/OYBtgujqVan+YNdyXmgK7sA27iLjbotK0ucZexPf1ZmCt9eAE0AHdfF+CJd/V0IfxKCO/9/KZlKT+boCyOILyv2ilHQmyY2TZVShG2J/CD5+54irTA7mWVBYrpAECQQDGZyIWH4WhRipf+RnUsUuQWGBxEM5XmaNvt01LiDxEBmK2CMQI9BYozTCyn0V8skuQZcc2GNRi9k42e+3cdDrj4lAkEAwPP8Mn5vWIgrodAAHFOCQQ1kMU757RoX4jNCBp8243NyvC8v5Zkb+dNcqcYIe9NVkHa6JA1/2Ia8AGY80EKPq3QJALY4wvXOvc0BxKj87WFoQFSKkGuTd+XnJlTU5gkZd0CDHOZT5rjSdgfDbZag8hE1MHHPCMiGxYqe7fbcLuphO0IQJBAKyI+/xh/qjz19l4IyjCKE8zrlo3o2t7DGFwyDXYyZDhagVv4rdGWaNC/nbpF6jxvm8hW+yzlzU3mnLGWugZXnnRkCQGHJ3iEejJYUF6MAcjYS74eSJEXGyP1ylBb5jUZQ1o14+BImqfcScySTgkt4s4ix7VsKaio8k7AW1OLOJvNYj09Q=+-----END RSA PRIVATE KEY-----
+ test/keys/client/id_rsa_1024.pub view
@@ -0,0 +1,1 @@+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCVinDqmlOiQg+qIGP//qby2v9huGNp2z0pWngHGD1qdZtq5MkjPgYEbWO1xm0CIE2dLKwb704+U6Cj0ZM7vi1PwteojKG3Opt4O9vfdLpM6LSkIUPijGvIH0sbSwSTtOv2DPD60bvyeqwLDpfh+zZgLXC7dbi72W9IsN86k8h38Q== ganesh@paxton
+ test/keys/client/id_rsa_2048 view
@@ -0,0 +1,27 @@+-----BEGIN RSA PRIVATE KEY-----+MIIEpAIBAAKCAQEAu5uFyWkM9i5vOiI8TKZUy5dLX0fJ147qkicwy0J+3dbYFZz8+1OUC10OIJX0YO7i5QQdQoE4v1aq/v3KofZ47EnFna9tzz22rsEKVI/CvU+zIolSN+2hn+URpDpTGKlz3aKFaxuuVFNlHX5KBYrY9Se1G1rriDLUFxKBssJwtjjELWheZL+beyn2ZjEBuCbuFYsuQYQvVvPKqMnDBWVNARVYdwU3hqlPpEJi9eR5SZhKpLbJOuu+mL/f+FsAdcvQzul8DaCmab6ffxx4+oFFJEIJ1ui+eMlU7KUqBq0KKklteHycOHRX+SlYtwTwfJGobuCGQ40UrWOYLqaebbzebgIc0zwIDAQABAoIBAQCO1si0IwG1ZoFV+N7/FdFbXc+f1MYliT/QVNzWVbJl/ehanzhFKXtsc3tQIBwiuX+TcuB8RDrao7gp0+T24txo2fayydGEdCxXOVDfzTkmtLcNsJjWs7hdL2GRMr8V5d7L/vOqKzc+CQjRvd+fKBH9PmN3xZ/YwitKkhnitjBGZC/ts2K3AK8THZ6VgK5h70rvvUu1C3eUj6qF5IU+Pze/Kb5HXVLnykI4wcFFF/IrR0Yi7XE+OCpeALf5XqeUlHwoyLttPxsfog7vxJMp+tJrKrcgmnsTlhOCktJys8o34QQrqtDe89bMWXbyeBadfbcLi7S1DPNr8j2ZKKimX+1YnPJFTRAoGBAOmxUrZfJLUiwaJi755uhSgYXDVsHWucHwldLS3MQ+WbeTUbq8Sk+W+zrFMCVYMo9VtqHUrNx5WTIjggxFs6gXQ8DJXYK8obGXW4Pocv54d+vgFSTwwwE+TcAOsxZpUumQHABeuZinTK8DaGJR6/btbaYk7V1OBHbD+9ixEqtrZU7jAoGBAM2E+B8y2IXXiyRGIeHC8mtiJlwY528QZn1EaCRsFXLvnxv+00jaf9CZUmR74ZyFi/bJ4+V9oXFkrTlRq6gJJFA/G3BBvivIxfn0+iLOj2ScgELWfPZ+qu/zbR9qyZrczf4CEf+q74x/5k3SMwi4Z8w4X68HaJtIKv3SWFQsgRn7VolAoGATJ5dvvtcvqKhl8sWQvx7+XdT2znEfCDwMlPZerKhPDoW17Kqg230Dwp6klCulq0kHI+jAPaM3EZ8LqjXmA2Li+f9yJOLWIJJX+5ensI0NPQhZ5XcGAbn0uvKxVHSD1FSyxcZGdCia35p2YaLvxQGlB+zPpIdJHytrm05avQsjEo6v8CgYAVcjy6IRt2yNbArKQc56GlDR6keK81VJMqjHP8+zN9zgGlkz8LMDn8U7OkZsURZ2JGu6R4J1TTvVAsQQfwex3L062tTWaLhZy0hy0Oy+f9kNNVpjpeIrPF3Ho7uBIXxgj9A8xKhQbblFjN5c5xryWNB/QDQ7efXZ3DFdJWWe+ThYXvQKBgQDQ+O0mYnsK2qibTaVIatiLwmFby7LIYbmz/LmeOqUm98P5VVzSoOmx+54uONScpqp7BSKt7FMdqxVZzFVWQ6hZdI1O7NTQbst74R/RUDSE9+9stIXKlNNvz+3Gyh68aTZWxmgwV4eguhIPwfFJAnJ08R3zcV1OAAlb8trUNy6u4FuA==+-----END RSA PRIVATE KEY-----
+ test/keys/client/id_rsa_2048.pub view
@@ -0,0 +1,1 @@+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7m4XJaQz2Lm86IjxMplTLl0tfR8nXjuqSJzDLQn7d1tgVnPzU5QLXQ4glfRg7uLlBB1CgTi/Vqr+/cqh9njsScWdr23PPbauwQpUj8K9T7MiiVI3aGf5RGkOlMYqXPdooVrG65UU2UdfkoFitj1J7UbWuuIMtQXEoGywnC2OMQtaF5ktt7KfZmMQG4Ju4Viy5BhC9W88qoycMFZU0BFVh3BTeGqU+kQmL15HlJmEqktsk666Yv9/4WwB1y9DO6XwNoKZpvp9/HHj6gUUkQgnW6L54yVTspSoGrQoqSW14fJw4dFdKVi3BPB8kahu4IZDjRStY5gupp5tvN5uAhzTP ganesh@paxton
+ test/keys/client/id_rsa_4096 view
@@ -0,0 +1,51 @@+-----BEGIN RSA PRIVATE KEY-----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-----END RSA PRIVATE KEY-----
+ test/keys/client/id_rsa_4096.pub view
@@ -0,0 +1,1 @@+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDE7cf6+GqbGgS5fj9tF8Hf3e55JbPIf7OIZorlOIS8Nngh0W+hVWrLMTj3vxl00APMPWKGOtZlN+7LrvBe1GgHJffiEzY4x1CVqATqggsOFmJXppsCXJR+7PWeorV7lj3LDg6FtggEjG4qB/T+2pK0D94yaGc2+N4NcuORdxzk8Nf0iTmEs1Rxxu7ncBin8VZBxtkIu12XzwlOYq1H2aHMxDU+9G7cp1MmdCbkWUDa7VO6m7oLDrK6gwZIFto0B8KhqIJ0cNgmAx5hIBKhr+sOh3EvTEgUYGUTu/+9EN2RO7d9LoRzqQJTq73aW3Wj7YXNUhVt/XUf4ru/2/uJQxZIXhgtas/BcneNKWwp3ldPc8G2elDBlzCepEXF6FnzEDcMYlR8IthgTcXJbvZVWP7z5XfDQXqAnfE6zHZdOZER+0hJwzMq259Ld8KKrPYdPCGGcUnXYePPOZaTN8tCyJzDSg/W3QcceMq73kmPLz0q7FZ4UsB8BPsWMAJiuqkZO3lU8uTD5QDJM/3wq2b7OOAer8QW6emVS3gIa385uUzihB2H3zgP4ptSUt59UnqQxQzP3oaDU4PGMDTCFOnAGFNMPeIfYTqfNH0wqYk5OWG2635udZ/3/r1oHBIZ9fRBeHZi4ggIFcoQQIEvOO1nCTLoZqw4GhzT5E0ZTlRoz+gE8w== ganesh@paxton
+ test/keys/client/id_rsa_test view
@@ -0,0 +1,27 @@+-----BEGIN RSA PRIVATE KEY-----+MIIEowIBAAKCAQEApAocCmMKaBUJ8AZ5KQbqDZcCsLTFrRm9qTvDiVxS2ee+nhgI+sBuxYi0hhzjj2S7ZLgUYORIx2CPL//ldw7sjkUUnKQHxOM/wkScGp3C+QqtfABhI+hD2S+KbA3HWqL4ByN3cWb3zDmWOysGuRbIwEGYyTYrkayHU+ewPqGyInP54uH8Ut+1IlsDXO9waCiHxuBJFaE37Xycrtphetktv8JjvS3kfbSC6OZTsF0M7PvCnyUmvit+SfliHQtzI26lWGHR08aZduyBd+txvbJKDzV8vyrSA+zYk9uWnuEju47RJslE9yrg+IVtRnduRRszLXlnHCyWgm5lK3zlzf63S45Av1QIDAQABAoIBAE/1YY+qkSMExlBK+R3q5FRNEvZn2s7hZqLo6GGj3KKdwr9iN7IYzKXaqewJXt7BghppI/3KqLMOnR0Ph+gFPs/zxLUfhKKxO/QArw0+yAJy2GLQt1yrsy7FXpqm6LqEX5PTNOCBCV5x34m9wk+wsD/SuxNOnZPtf9qLud1lAJf1nPKVbGeZHZuXmvExRSLv/sDsy80W8kUapDLZgMm+6z6G6rR9+iofNdfz9O/xieHcsLuAuShIltnM9Mu2BHxYreZ7L6g4tLfTZXa6dhIJ+2OU2/qHrmx5wIqy4JaK7A64CM/Wnx4zOrVhWiv25AsPG3MHiWxydHe4nkNOey/cT+U3IwZcECgYEA1/llem5k9Most+FvLYjoebfnFCl29oUgavidnwLp7lh4cZ1SY9+i+TTr84CiNgmtINbf0AbWzEPiYWAlYGsctfVNZSIa240wIrfmy9oDa5wxz9QjF7cDY+i0txG8LIYlN/vM4T72bwl1zw+cJRf591zCK8y3moVSIRZppYbNlD4HECgYEAwnC9+r4eeCTdwhwoK33XVkh5kkM/CoRMFpKg04ubttW9D/L2yZg15ZHjRWHti7GyLvVIv+9aW678cXNdlV30WBcABtmN2d17mwrBbDqAdc2pfbtdFBBGnVIWekpXNEP9mINLly+qi7NRTG68jHvSn1SKVkQt3zKF+BGnDcWeGM8d6UCgYBjrKAe3vAM4Xm963bKBxNz+iWJGNdTHdS0+8TqddlTMQVxk4vxxR3a6Oe0W7uBQPn72+8zLNTZNMM3uY3Gb+iyO+WHcuN64UPLUMxd7IUTO1ylOB1Oi0D3pg3xJ2g3DDoFGlq14b8OA8mxJD0mCWi9tr+uOR069K6Z5ysQ7NnmOXwoQKBgArUQJxAD1swDUJYGtbrNyPWMX+nMo3KA2xyOc/R+ULfkJIM1BXSNl48y82XcKVxFh1rZ8vXZbxnfmrlTC2dN9bGJNJFo9luHagGLmwYG+svMxtfjgWKCoTEh5/z9/tfNgaCeXlH1J8gDCjkji4xLg++x1m2q8tnyx7vzQTJ4T+2NBJAoGBAJVakHmGKh8NM9Oa4QZe3CT110HQidVTzZNZ5dzMpU5HhS7dAa8YbVp5+IjrGXt/o0Yuid8T6UKmGFpiAL/gGx/fOVCMG9j2O9qF980082VWp+wRzVydnmC2c+nk6U1NvEhanPSa3IbNIgFUOc6Of3Jhs44EH3jw7fraITEX14bTqH+-----END RSA PRIVATE KEY-----
+ test/keys/client/id_rsa_test.pub view
@@ -0,0 +1,1 @@+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkChwKYwpoFQnwBnkpBuoNlwKwtMWtGb2pO8OJXFLZ576eGAiwG7FiLSGHOOPZLtkuBRg5EjHYI8v/+V3DuyORRScpAfE4z/CRJwancL5Cq18AGEiEPZL4psDcdaovgHI3dxZvfMOZY7Kwa5FsjAQZjJNiuRrIdT57A+obIic/ni4fxS3UiWwNc73BoKIfG4EkVoTftfJyu2mF62S2/wmO9LeR9tILo5lOwXQzs+8KfJSa+K1J+WIdC3MjbqVYYdHTxpl27IF363G9skoPNXy/KtID7NiT25ae4SO7jtEmyUT3KuAhW1Gd25FGzMteWccLJaCbmUrfOXN/rdLjkC/V ganesh@joyful.com
+ test/keys/client/id_rsa_test2 view
@@ -0,0 +1,27 @@+-----BEGIN RSA PRIVATE KEY-----+MIIEogIBAAKCAQEAwnECqcGljJ68vz/uoFsJxrlSPZOGLCCJD6DCv9NZlKLZERh5+vRLK9WpdRrgT59gyPRmyQWNugZ3uPSTnM7qi7RlSaCZkt6pvQqfuDnoxe3QaFrZw+3WoCM8hk3SolMyD/kMQW0Oss1mSJwjVrXRIctXf2Dh2kTEcBMDF+C+P1SIoDkL7x+jea5XcbX/tt2kBsSAYhtnWiDiSgiraXtRGk0ox3rRdmH4Jd11WFYuTDtmqlTCcVe+DhzsbT19th81msxSRRwnOj7foRwo1xVu2VrqNNMbYf7ley0pnYXbVmaBQui9X4Wg+Rw6CZEFpzhJ4yXMsIr1i1cOC3WuJ9sUqucZ/EQIDAQABAoIBABp5Po6UYhDqT/KO+JtRIcOVQuCTQIDWD3IV3MuzhPtIg9gMA3RpkI6QUbkzBpVwssHJnPEDw48vcD+Ld+UmlVoPc9Ol4Z1B65otpleOEZvAD+BstO3cEXvOMGBs2h/lyewo0YVa7uRjWOjL8X+fN24KJaAlczINmV4SW4hXvMJf4z3mGvr0/vDpABht9zHftp6BiWXZYl+Kx2UrWaW+a7n5g0iTuC9dEINqQVUazA3ty91+YUSxrD7XtwklwTRBSpuZM+//SbZGdnQik2PM+PSfPLu5ovZ0MwI9LZJPQ3eRLGo89Em4wXHvKLzmb0mX9fvhP/244A4QQVjZobMN3+rCX2Vw0CgYEA+h/pyX/bWEzAm7xyOuLYrde42R2XfjBdLcjD7j01pvPxc7rgpYRb+qVEOKXvoT5y9dX7PslwzSqwmUhbmdN6aX1YUvs6yPXBF851yMbY2qW+jn2ZAWJD4+sQ25p7wV4GDRYJprLgnnUg9nHThuEbz9CoZTegKb2GF7wuXVw6lwxMcCgYEAxwJB+LTsR11NdC0V/0pYjiB9wLrXvILlly+rTpGoApnDqoWYn6RayJuiQUXITzhFdtrkm+7tfe2RKEuh/aXfV+iR6ed/QxwHED5Vd8XYbbpXRuzWH0umpr50x6ePESxvk51BVw+5fg4hF6/2Mwn8map1n6e759CVvJK+laNlL+lFWcCgYBdm7n0FmyxtC/VWQZrMWCk+VvqwDtoWeOU2cE+bhr7gl6VCiarvZwSi7lndfqjnuqJRKb9zYfw0Mw84Y6emD3U4+vs+OxW6BfdZAISmOn0H/0W8sBamJO+BG6vsTYlnRmophnAkGtuAinu8ZSXgwHUma+OcFeBUHsDjeyLi/9RRmWGwKBgH1+/nr3dRjEiThCa4jxBRciPCw4rsOEJp2hSDW2+YxKSwmNleGWU2mOO5PN3bOXWLbK8r8COgQmClBCLZbk6xsDRfj1G0Nj6a+qEcPjJ+wllkQzthOmMUGVeS8uixnZW8NKt5mehrz7gpx/F/TPGfrBqHXtLdK4iI4p9bVY0o+DYKhAoGAWmSgza5IphlKzx0YNdSTzPZ6RbHIk/HWEIAtDlAV60ucTtxOMUdjDs0Z+KuGXxwT3ahf1LK23PHDfC5wDBzMffSkm5w5AK/+1PdtTaVLRI2g3l6RCm6L15vhO+AZ+26PUzo9zmIGeHXJl6naTiQUW234/WTt3pyFdQ28k8jhXnu7s=+-----END RSA PRIVATE KEY-----
+ test/keys/client/id_rsa_test2.pub view
@@ -0,0 +1,1 @@+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCcQKpwaWMnry/P+6gWwnGuVI9k4YsIIkPoMK/01mUotkRGHm9Esr1al1GuBPn2DI9GbJBY26Bne49JOczuqLtGVJoJmS3qm9Cp+4OejF7dBoWtnDdagIzyGTdKiUzIP+QxBbQ6yzWZInCNWtdEhy1d/YOHaRMRwEwMX4L4/VIigOQvvGN5rldxtf+23aQGxIBiG2daIOJKCKtpe1EaTSjHetF2Yfgl3XVYVi5MO2aqVMJxV4OHOxtPX22HzWazFJFHCc6Pt+hHCjXFW7ZWuo00xth/uV7LSmdhdtWZoFC6L1fhaBHDoJkQWnOEnjJcywivWLVw4Lda4n2xSq5xn8R ganesh@paxton
+ test/keys/host view
@@ -0,0 +1,27 @@+-----BEGIN RSA PRIVATE KEY-----+MIIEogIBAAKCAQEAszT/56/xQlb08L+WdJv04XAxV3LFqNk73HGCd2MOul6Q5SiD+LT+s2fisK2PFYodwRD1yId55ouy3xCVNkj1FdYrX4Fa3nddPt8qzhwrKLhmoWVEo+4e59kjz8q5qhCFOHstYsHtKHNK1vfJiZaYlIuG9/XmBZHTLVmzvwkhn0nXKthMFS+q5h8MLvjP00/EY438luYxkl6rao5O7JKOFnM/A1/oznxhYpbd/zQndyO7z5AchO9+ODGNC+hbECkIC8c2V6XbJChfolHTUe6Wz809EdvyzwXCI5XU/Wk3r6P6pVa1A/qu+J6lg33bic/FbZUxMhVJgxtWUHi/goON5UiPczwIDAQABAoIBAA9/sfIzwUXfh2ea+6kRDiLZGob3Sa03jG85A8uuoYYm0zAtFqbKsIyLm4t0Nz/BpjkgYmFpdh1+T4YDR+SRgwvGUiEPGSDmdUS8y1dzlisYGzLmArKMIBglK3e5LL2MmDj+TjqQUxaAgiR9Ya+zInYGbX5zqY25v4je13lnTTFkeZb859N3XAVbay+82IJej3tFNSV1tR+H6FtRIZ/+UBASKwAaw8Cbjys0E8gaSPBk7ZzPTD4h7EOAYvTGZgrxirV5HBjusg2AIIvcOY2m+UUSRuvvJzjff1mxygJeBkNMDnRNYQRqrDXFzNs08poiVZ7u6yPhN4D4o/Xr+4sQn+ACbTnIECgYEA2p8qnuJtnnvXIpwctzccykg/cjaBPIG3UeYM1TQSzbYdZ0KfZl7D+OUGxJ+WKeHulXnJY78gVFKpqU0UMP5lqZSyYPA4TwOnIBZNiJ1dBh6nfhrrXd45g+t65n52sx+b6o03natfKXn/2+Er0GS3K9W0YgnCFyBCIGXAs+mpqovvECgYEA0dix+xK4egGLYGajuk3FT2xI7bop/+NE3GzIQHQ+9MWUc5/zomakR+V/BOU9zmbcfynK2+5nxd8KZxzHmm6nS4KWuaw+7HNCFJyPZhIAd+5tbgsKewoGKUdTNyZNY4jGuWX981+0yYLazoXTwbGMVGneyJXJAjg9YS6JlJbgcXx178CgYA8T+zSKGVVc5TGV4sUgH/Q+zl2yhJbiumZ4kZ64ssT9O2ChPB/9fecdxKG6//hThMj6ZVFj1S77pIfwsPvQD+Lq+RoM5Dikk71nfL+nEMK5DXvnrkWAf+4dzJQpFLa172L16mgNcbrCl0rq9MKir59uV+cqNMb22k9j4K5o8+16v2AQKBgA0LdXGOiWLdwiVGNR6BSv8xUPR8M5xaFRzhrdLA+qbgqWvOo6ySyN+XSqAZSBBOoKJfDLc+CJ6zQC/70CQZGHzSj9cj8TPHWp+mQN1Vw+Ydkjvm/83KP7vNLUUeYm0vkXrw9ipsvrb4ZI5C4Lc8KZGtoytkwNKT7Z82ByejHF+BlWtAoGAZdrfpNdPLYH/t570CtOMuCVAbNop56Yq9f3EmJefU/urFmu9tBLw8T1r+2EmcPovH20MqjOdPVFqHe4tMN5c6xI4OTp1O4gFpYmzUsMIwVkbTuSMCdc96TdJw+C3y11k+V3LDZ/DVMzr59LZd48LOQFYHUMFMwsyf7fU5oiuSae+Y=+-----END RSA PRIVATE KEY-----
+ test/keys/host.pub view
@@ -0,0 +1,1 @@+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCzNP/nr/FCVvTwv5Z0m/ThcDFXcsWo2TvccYJ3Yw66XpDlKIMtP6zZ+KwrY8Vih3BEPXIh3nmi7LfEJU2SPUV1itfgVred10+3yrOHCsouGahZUSjh7n2SPPyrmqEIU4ey1iwe0oc0rW98mJlpiUi4b39eYFkdMtWbO/CSGfSdcq2EwVKrmHwwu+M/TT8RjjfyW5jGSXqtqjk7sko4Wcz8DX+jOfGFilt3/NCd3I7vPkByE704MY0L6FsQKQgLxzZXpdskKF+iUdNR7pbPzT0R2/LPBcIjldT9aTevo/qlVrUD+q4nqWDfduJz8VtlTEyFUmDG1ZQeL+Cg43lSI9zP ganesh@paxton
+ test/test.hs view
@@ -0,0 +1,266 @@+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+module Main where++import Test.Tasty+    (TestTree, defaultMain, testGroup, withResource+    )+import Test.Tasty.HUnit (testCase)+import Test.Tasty.QuickCheck (testProperty)+import Test.HUnit (assertBool)+import Test.QuickCheck+    (Arbitrary(..), elements, forAll, choose, vectorOf+    )++import Control.Applicative ((<$>))+import Control.Concurrent (forkIO, killThread)+import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar)+import Control.Exception (bracket, try, catchJust, ErrorCall(..), evaluate)+import Control.Monad (when)+import Data.ByteString.Char8 (pack)+import qualified Data.ByteString.Lazy as LBS+import Data.List (isSuffixOf)+import Data.Map (Map)+import qualified Data.Map as Map+import Data.Word (Word8)+import System.Directory (createDirectoryIfMissing, removeFile)+import System.FilePath ((<.>))+import System.IO (hPutStr, openTempFile, hClose)+import System.IO.Unsafe (unsafePerformIO)++import Network.SSH.Client.LibSSH2+import Network.SSH.Client.LibSSH2.Errors+import Network.SSH.Client.LibSSH2.Foreign++import qualified SSH+import SSH.Channel+import SSH.Crypto hiding (sign, verify)+import qualified SSH.Crypto as Crypto+import SSH.Session++import EmbedTree++keysDirectory :: Map String Entry+keysDirectory = getDirectory $(embedTree "keys")++sshPort :: Num a => a -- used as an Int or a PortNumber+sshPort = 5032++withOneUserServer :: KeyPair -> PublicKey -> TestTree -> TestTree+withOneUserServer hostKp acceptedKey test = do+  withResource+    (do startedSignal <- newEmptyMVar+        tid <- forkIO $ SSH.startConfig (config startedSignal)+        takeMVar startedSignal+        return tid+    )+    killThread+    (\_ -> test)+    where+      config startedSignal =+        SSH.Config+          { SSH.cSession = session+          , SSH.cChannel = channel+          , SSH.cPort = sshPort+          , SSH.cReadyAction = putMVar startedSignal ()+          }++      session =+        SessionConfig+          { scAuthMethods = ["publickey", "password"]+          , scAuthorize = sshAuthorize+          , scKeyPair = hostKp+          }++      channel =+        ChannelConfig+          { ccRequestHandler = channelRequest+          }++      sshAuthorize (PublicKey "testuser" k) = return (k == acceptedKey)+      sshAuthorize _ = return False++      channelRequest wr (Execute "check") = do+        channelMessage "checked"+        when wr channelSuccess+        channelDone++      channelRequest wr cmd = do+        channelError $ "<" ++ show cmd ++ "> not supported"+        when wr channelFail++withTextInTempFile :: String -> String -> (FilePath -> IO a) -> IO a+withTextInTempFile nameTemplate contents action = do+  let tempFolder = "temp"+  createDirectoryIfMissing False tempFolder+  bracket+    (do+       (f, h) <- openTempFile tempFolder nameTemplate+       hPutStr h contents+       hClose h+       return f+    )+    removeFile+    action++data AuthResult = OK | Error ErrorCode+   deriving (Show, Eq)++authWith :: String -> KeyPair -> IO AuthResult+authWith publicKeyText privateKeyPair = do+  withTextInTempFile "private" (printKeyPair privateKeyPair) $ \privateKeyFile ->+    withTextInTempFile "public" publicKeyText $ \publicKeyFile ->+      withSession "localhost" sshPort $ \session -> do+        authResult <- try $ publicKeyAuthFile session "testuser" publicKeyFile privateKeyFile ""+        case authResult of+            Left e -> return $ Error e+            Right () -> do+              channel <- openChannelSession session+              channelExecute channel "check"+              checked <- readChannel channel 20+              when (checked /= pack "checked\r\n") $ fail "incorrect check result"+              return OK++breakPrivateKey :: KeyPair -> KeyPair+-- This leaves enough information intact to reconstruct the private key+-- (e.g the primes), but in practice it seems to be enough to cause an+-- authentication failure.+-- Changing the numbers too much can cause segfaults or out of range signatures+breakPrivateKey kp@RSAKeyPair {} =+   kp+   { rprivD = rprivD kp - 2+   , rprivPrime1 = rprivPrime1 kp - 2+   , rprivPrime2 = rprivPrime2 kp - 2+   , rprivExponent1 = rprivExponent1 kp - 2+   , rprivExponent2 = rprivExponent2 kp - 2+   , rprivCoefficient = rprivCoefficient kp - 2+   }+breakPrivateKey kp@DSAKeyPair {} = kp { dprivX = 1 }++publicKey :: KeyPair -> PublicKey+publicKey (RSAKeyPair { rprivPub = k }) = k+publicKey (DSAKeyPair { dprivPub = k }) = k++hostKeyPair :: KeyPair+hostKeyPair = parseKeyPair . getFile $ getEntry "host" keysDirectory++clientKeysDirectory :: Map String Entry+clientKeysDirectory = getDirectory $ getEntry "client" keysDirectory++getClientPublicKeyFileText :: String -> String+getClientPublicKeyFileText keyName = getFile $ getEntry (keyName <.> "pub") clientKeysDirectory++getClientPrivateKeyPair :: String -> KeyPair+getClientPrivateKeyPair keyName = parseKeyPair . getFile $ getEntry keyName clientKeysDirectory++privateKeyPairFiles :: [String]+privateKeyPairFiles = filter (not . isSuffixOf "pub") $ Map.keys clientKeysDirectory++singleKeyAuthTests :: TestTree+singleKeyAuthTests =+  testGroup "Single key auth tests"+    [+      let publicKeyFileText = getClientPublicKeyFileText privateKeyPairFile+          privateKeyPair = getClientPrivateKeyPair privateKeyPairFile+      in+        withOneUserServer hostKeyPair (publicKey privateKeyPair) $+          testGroup ("Check auth with " ++ privateKeyPairFile)+          [+            testCase ("Works") $ do+              authWith publicKeyFileText privateKeyPair+                >>= assertBool "should auth with correct private key" . (==OK)++          , testCase ("Fails with broken private key") $ do+              authWith publicKeyFileText (breakPrivateKey privateKeyPair)+                >>= assertBool "shouldn't auth with broken private key" . (==Error PUBLICKEY_UNVERIFIED)+          ]++    | privateKeyPairFile <- privateKeyPairFiles++    ]++wrongKeyAuthTest :: TestTree+wrongKeyAuthTest =+  withOneUserServer hostKeyPair (publicKey rightPrivateKeyPair) $+  testCase "Check auth failure with wrong key" $ do+      authWith wrongPublicKeyFileText wrongPrivateKeyPair+        >>= assertBool "shouldn't auth with wrong private key" . (==Error AUTHENTICATION_FAILED)+  where+    rightPrivateKeyPair = getClientPrivateKeyPair "id_rsa_test"+    wrongPrivateKeyPair = getClientPrivateKeyPair "id_rsa_test2"++    wrongPublicKeyFileText = getClientPublicKeyFileText "id_rsa_test2"++instance Arbitrary LBS.ByteString where+  arbitrary = LBS.pack <$> arbitrary++instance Arbitrary KeyPair where+  arbitrary = elements $ map getClientPrivateKeyPair privateKeyPairFiles++instance Arbitrary PublicKey where+  arbitrary = publicKey <$> arbitrary++-- QuickCheck tests end up using unsafePerformIO because sign and verify+-- are in IO, which in turn is because the DSA operations are in IO,+-- but hopefully they only have benign side-effects if any++sign :: KeyPair -> LBS.ByteString -> LBS.ByteString+sign kp message = unsafePerformIO $ Crypto.sign kp message++verify :: PublicKey -> LBS.ByteString -> LBS.ByteString -> Bool+verify key message sig =+  unsafePerformIO $+    catchJust+      sigErrors+      (Crypto.verify key message sig >>= evaluate)+      (\() -> return False)++  where+    sigErrors (ErrorCall msg)+      | msg == "signature representative out of range" = Just ()+    sigErrors _ = Nothing+++signThenVerifyTest :: TestTree+signThenVerifyTest = testProperty "signatures from sign work with verify" $+  \kp message -> verify (publicKey kp) message $ sign kp message++signThenMutatedVerifyTest :: TestTree+signThenMutatedVerifyTest = testProperty "mutated signatures from sign fail with verify" $+  \kp message ->+    let sig = sign kp message+        actualSignatureLen = fromIntegral $ actualSignatureLength (publicKey kp)+    in forAll (choose (LBS.length sig - actualSignatureLen, LBS.length sig - 1)) $ \offset ->+       forAll (choose (1, 255 :: Word8)) $ \mutation ->+       let mutatedSig =+             LBS.take offset sig `LBS.append`+             LBS.pack [LBS.index sig offset + mutation] `LBS.append`+             LBS.drop (offset+1) sig+       in not $ verify (publicKey kp) message mutatedSig++randomVerifyTest :: TestTree+randomVerifyTest = testProperty "random signatures fail with verify" $+  -- might be sensible to test some other lengths, but the actual code+  -- just takes the last n bytes anyway, and it's not totally obvious+  -- what would be a good range of values to test with.+  \key message -> forAll (vectorOf (actualSignatureLength key) arbitrary) $ \sigBytes ->+    not $ verify key message (LBS.pack sigBytes)+++allTests :: TestTree+allTests =+  testGroup "Tests"+  [ testGroup "With server"+    [ singleKeyAuthTests+    , wrongKeyAuthTest+    ]+  , testGroup "Signatures"+    [ signThenVerifyTest+    , signThenMutatedVerifyTest+    , randomVerifyTest+    ]+  ]+++main :: IO ()+main = defaultMain allTests