packages feed

smith-cli-0.0.1: src/Smith/Cli/Agent.hs

-- |
-- Agent operations.
--
-- FUTURE: This should move to openssh-protocol.
--
{-# LANGUAGE OverloadedStrings #-}
module Smith.Cli.Agent (
    Agent
  , AddKeyError (..)
  , Message (..)
  , messageByte
  , Reply (..)
  , replyByte
  , connect
  , addKey
  , packet
  , send
  ) where

import           Control.Monad (void, unless)
import           Control.Monad.Trans.Bifunctor (BifunctorTrans (..))
import           Control.Monad.Trans.Except (ExceptT (..))
import           Control.Monad.IO.Class (MonadIO (..))

import qualified Crypto.OpenSSH.Protocol.Decode as Decode
import qualified Crypto.OpenSSH.Protocol.Encode as Encode

import qualified Data.Attoparsec.Text as Parser
import           Data.Bifunctor (Bifunctor (..))
import           Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString

import qualified Data.ByteString.Base64 as Base64
import qualified Data.Char as Char
import qualified Data.Serialize as Serialize
import           Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import           Data.Traversable (for)
import           Data.Word (Word8)

import           Network.Socket (Socket)
import qualified Network.Socket as Socket hiding (recv)
import qualified Network.Socket.ByteString as Socket

import qualified OpenSSL.RSA as OpenSSL

import           Smith.Client.Data.Certificate (Certificate (..))

import qualified System.Environment as Environment
import           System.IO (IO)


data DeconstructedCertificate =
  DeconstructedCertificate {
      _certificateType :: Text
    , _certificateBlob :: ByteString
    , _certificateComment :: (Maybe Text)
    } deriving (Eq, Ord, Show)

data Agent =
  Agent Socket

connect :: IO (Maybe Agent)
connect = do
  path <- Environment.lookupEnv "SSH_AUTH_SOCK"
  for path $ \address -> do
    socket <- Socket.socket Socket.AF_UNIX Socket.Stream 0
    Socket.connect socket $ Socket.SockAddrUnix address
    pure $ Agent socket

data Message =
    AddIdentityMessage
    deriving (Eq, Ord, Show)

data Reply =
    FailureReply
  | SuccessReply
  | ExtensionFailureReply
  | IdentitiesAnswerReply
  | SignResponseReply
    deriving (Eq, Ord, Show)

messageByte :: Message -> Word8
messageByte m =
  case m of
    AddIdentityMessage ->
      17

replyByte :: Reply -> Word8
replyByte r =
  case r of
    FailureReply ->
      5
    SuccessReply ->
      6
    ExtensionFailureReply ->
      28
    IdentitiesAnswerReply ->
      12
    SignResponseReply ->
      14

packet :: Message -> Serialize.Put -> Serialize.Put
packet message payload =
  Encode.string . Serialize.runPut $ do
    Serialize.putWord8 $ messageByte message
    payload

data ProtocolError =
    InvalidResponseHeader ByteString
    deriving (Eq, Ord, Show)

send :: Agent -> Message -> Serialize.Put -> ExceptT ProtocolError IO ByteString
send (Agent agent) message payload = do
  liftIO . Socket.sendAll agent . Serialize.runPut $
    packet message payload
  header <- liftIO $ Socket.recv agent 4
  size <- ExceptT . pure . first (const $ InvalidResponseHeader header) $
    Serialize.runGet (Decode.uint32) header
  liftIO $ Socket.recv agent (fromIntegral size)

data AddKeyError =
    IncompleteKeyPair
  | InvalidCertificate
  | CouldNotAddKey
  | CouldNotAddCertificate
  | AddKeyProtocolError ProtocolError
    deriving (Eq, Ord, Show)

addKey :: Agent -> Certificate -> OpenSSL.RSAKeyPair -> ExceptT AddKeyError IO ()
addKey agent cert keys = do
  (DeconstructedCertificate t blob comment) <- maybe (left InvalidCertificate) pure $ deconstruct cert
  let
    n = OpenSSL.rsaN keys
    e = OpenSSL.rsaE keys
    d = OpenSSL.rsaD keys
    p = OpenSSL.rsaP keys
    q = OpenSSL.rsaQ keys
  iqmp <- maybe (left IncompleteKeyPair) pure $ OpenSSL.rsaIQMP keys

  rkey <- firstT AddKeyProtocolError $
    send agent AddIdentityMessage $ do
      Encode.string "ssh-rsa"
      Encode.mpint n
      Encode.mpint e
      Encode.mpint d
      Encode.mpint iqmp
      Encode.mpint p
      Encode.mpint q
      -- FUTURE Better default comment, server should send one.
      Encode.text $ maybe "" id comment

  unless (rkey == ByteString.singleton (replyByte SuccessReply)) $
    left CouldNotAddKey

  rcertificate <- firstT AddKeyProtocolError $
    send agent AddIdentityMessage $ do
      Encode.text t
      Encode.string blob
      Encode.mpint d
      Encode.mpint iqmp
      Encode.mpint p
      Encode.mpint q
      -- FUTURE Better default comment, server should send one.
      Encode.text $ maybe "" id comment

  unless (rcertificate == ByteString.singleton (replyByte SuccessReply)) $
    left CouldNotAddCertificate

deconstruct :: Certificate -> Maybe DeconstructedCertificate
deconstruct certificate =
  either (const Nothing) Just . flip Parser.parseOnly (getCertificate certificate) $ do
    t <- Parser.takeTill (Char.isSpace)
    void $ Parser.takeWhile (Char.isSpace)
    base64 <- Parser.takeTill (Char.isSpace)
    blob <- case Base64.decode . Text.encodeUtf8 $ base64 of
      Left e ->
        fail e
      Right v ->
        pure v
    void $ Parser.takeWhile (Char.isSpace)
    comment <- Parser.takeTill (Char.isSpace)
    pure $ DeconstructedCertificate t blob
      (if Text.null comment then Nothing else Just comment)

left :: Monad m => x -> ExceptT x m a
left =
  ExceptT . pure . Left