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 +62/−21
- src/SSH/Crypto.hs +111/−27
- src/SSH/Session.hs +2/−2
- src/SSH/Util.hs +8/−0
- ssh.cabal +50/−9
- test/EmbedTree.hs +57/−0
- test/keys/client/id_dsa +12/−0
- test/keys/client/id_dsa.pub +1/−0
- test/keys/client/id_dsa2 +12/−0
- test/keys/client/id_dsa2.pub +1/−0
- test/keys/client/id_rsa_1024 +15/−0
- test/keys/client/id_rsa_1024.pub +1/−0
- test/keys/client/id_rsa_2048 +27/−0
- test/keys/client/id_rsa_2048.pub +1/−0
- test/keys/client/id_rsa_4096 +51/−0
- test/keys/client/id_rsa_4096.pub +1/−0
- test/keys/client/id_rsa_test +27/−0
- test/keys/client/id_rsa_test.pub +1/−0
- test/keys/client/id_rsa_test2 +27/−0
- test/keys/client/id_rsa_test2.pub +1/−0
- test/keys/host +27/−0
- test/keys/host.pub +1/−0
- test/test.hs +266/−0
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-----+MIIJKQIBAAKCAgEAxO3H+vhqmxoEuX4/bRfB393ueSWzyH+ziGaK5TiEvDZ4IdFv+oVVqyzE4978ZdNADzD1ihjrWZTfuy67wXtRoByX34hM2OMdQlagE6oILDhZiV6ab+AlyUfuz1nqK1e5Y9yw4OhbYIBIxuKgf0/tqStA/eMmhnNvjeDXLjkXcc5PDX9Ik5+hLNUccbu53AYp/FWQcbZCLtdl88JTmKtR9mhzMQ1PvRu3KdTJnQm5FlA2u1Tupu6+Cw6yuoMGSBbaNAfCoaiCdHDYJgMeYSASoa/rDodxL0xIFGBlE7v/vRDdkTu3fS6E+c6kCU6u92lt1o+2FzVIVbf11H+K7v9v7iUMWSF4YLWrPwXJ3jSlsKd5XT3PBtnpQ+wZcwnqRFxehZ8xA3DGJUfCLYYE3FyW72VVj+8+V3w0F6gJ3xOsx2XTmREftIScMz+KtufS3fCiqz2HTwhhnFJ12HjzzmWkzfLQsicw0oP1t0HHHjKu95Jjy89KuxWeFLA+fAT7FjACYrqpGTt5VPLkw+UAyTP98Ktm+zjgHq/EFunplUt4CGt/OblM4oQdh984+D+KbUlLefVJ6kMUMz96Gg1ODxjA0whTpwBhTTD3iH2E6nzR9MKmJOTlhtut+bnWf+9/69aBwSGfX0QXh2YuIICBXKEECBLzjtZwky6GasOBoc0+RNGU5UaM/oBPMCAwEA+AQKCAgEAnXK3hzMCmQuOZ3hG4cQy3/gi6H16Zn2jXxUNTAOKLKkoF3HJ3KXcgqTS+NVJ22exOfQc/NK7qJ3c212cBC/CrU7vJmtldarwA9AaoF47to8/FxOqR2KuIloqC+Ptk8XJlcwnJ0rfdCJdDZa4V9Hh5HWOuu7YKHs236q6oFxJIFag4du4fTkwOGKomx+DR2UuQG9w01mzRJw3CVN/XvrqxyXvo3JPJJV6NlnI8GOLtglgGRrozFK5/hScci8+Rvrubtcdh/6gftSSHKI1sdXgz6FFQBKOLhDBOHlxlvuiltlbLPkf1VhR0d942sjf+PCwPvIb1iZltRMl20Y2IKveI/s/CiaRD7kAdjwXiuk0jvhGGlHCkABKdo+zHnLy7+1xx5Ef1IYr9fGx0PL2ELHdU+J0QXxKRZOgWWI9j6q55UPsQmC3Ue9zCPqhbhLLb8+Nsi95uqmcBY1J/EmUMs/ENVD4Xl9yeIhqyv/Mp1coUT/Zmj9S65KlszaJfLQVIMd+EVdyT/P5tTvf48sT+TK2RljDNhvHAIFlzjsXGKPN3HmTyVsDHdaqCVnr+K4aCRxi+iP8KraTeHiRozXUJMSGSBPpsmEvlnyuTRSkZ5Ox1QG9G7VxZHgmHQBdUidO3QvFj+3ji+1NYS4WXrxkJNb3F667Y5XRHPAkAUmT0LIFZOUwwRxINIpyECggEBAPuZqRhy+CQHmVhH9YTJP4FqHo4n9gjhc1Gm43CKr86P4lwlLlFgDfe+qXCeqG63+C4pzXaQn+vUaxvt3PtMDk6azRSY1tBgJRUZOYgPZuTkbTN3+a+HQhXUyVHKVLg1hLLg2PZcnZ+Rpcdpr2mpOFh/bzIgMQOVsxbVsFMbfWT6x64QasPs2Ij/LPenwzqj45fCZULec6R+gEIT+xh4jX5UajqewD8rRYQ4Q4qFPf593vl0q1oxCDmK6Wfwh5mOlj6wWGo6b6Ri+Bo8e8bVT7Y+mAjddhwZNkREdP3AyUHi/JI82J31fm2Dg7WCnprwAwUYLqyVtXRhG+G4cJxhHpKfxNWWsCggEBAMhfX35FD87mtrdKxT7tt0YOQ/VtX5+nHh07yEO/uwMD+qffAdUpnsmYbgrIRzL7QmCvP9kQPovP6aEvy3A/wZI+Rw1i2VKhQh0g6bPdDL5Ck++g6cKu3OoVQup8T78rFPxz8fZVnVf9UJIC5DoLbNU8Pfb3HsogQwg4vmXvtpyJYg+QJUqylGYrmk9iVcRu4hHjBkd+9D3vOKCE/27BK/GcjNamypqcjOJOAGFyczXRs+S+ielsgLsFRXCNmVSQ3QZ3zACrZ+CTJqvJffeGF4RL+mtOV758hVDguY7ZaIPbS+0w+rbbZUdXF9ebxJQyJ5st35E+B8HKCtYEK4Q5BSMnJvJkCggEAXQ0RetcB+c/kWTbq+sZ7VDOZJV4mIlavPa2JRGAmcTDJuOaPYM0znULIi7xB8uDbSsdvE8Cc6W2D1hDeD+VCvVOHMWztVZeZX1o66tU3asQIlxZyI7bUfBp8cmFwP8ibUUTTORo7tV3iG7PzzY+kfqZyy4kYV4kP+QwC8FmkYKpXG0s7EUcRNmmZieZjz9Y5IDFnHfoDrvFQar+HKjJ+O8WgnBmGZFZumV3trNdmfC61PnElxm+H6TA07poIrIQNkRXLPU5rZ9JRNrFtF3D4+1T3CaBOREoWxdzDn+2jAVkfrD4QpyraHUqcdY7fddH6a/HroSylNWuLi32h/9rPT+MVqyDQKCAQAaO4JBAcGkEMhzDrLsHisUXOHAy+Ts/fAPW7hIRl2xc1VZPjUc3J6a+h5eAwJvRj3WcpslS98kZr/rflpgA7jP8J9UvVA+ZSZGsfxms9XrQsQibyQ5Fu/ub+DdChFWsck5k+Rln6fN0TgvJXnDr6M793sVTh0V0Ut1VBh5N/zsWYAfjyjnuWWyra+VFgashOfL97Dmw1Qul5cOTNs0IM4j/57gq01zGHPJXPTnzRgQP7gRgsQKEhiM7p3+ldIxJBYLtrtaixY6bIlvjB2VvlRt2ZSbX9JU+fBqOkGQ1h28xYUXNHxJqAHyvswG+xCNZlORGVxfo4NYd58bjcg0s1Jc289ZhAoIBAQDnsQUF4Wl5XGWcaPySMGon9rG7+gI1X01dW4v1xyPo+fN1TaIp/YzHE8WaPpB2qJZLsKnAGZyBf+9EgL72SBRlwSqq1+Zt7o9TDxL/Ut/nxLGG54j+6JEXGnTzTLdLBKLwG6th7qsKwocNdaMMoI8u5ljVwA+0wb996ww2CUwFpRB8ww6cS7oSyw13usSXeqJUk+sskQ4CKmTroUWhEg3ptDWvoyA+0GuIcN7c9l91br3fRLZKzKXk6zmHj9JAq3rY+kLTtZEpWwmbSUFCICnwpk3ffSV2+F/xxheoW7u2Js0bFDxdwnmEZZf5cInqgtSp9iq2sbb5U2wvs7jn8oPF/JhKj+-----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