diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Revision history for smith-cli
+
+## 0.1.0.0  -- YYYY-mm-dd
+
+* 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) 2019, HotelKilo
+
+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 Mark Hibberd 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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,51 @@
+# smith-cli
+
+This is a command line interface for the [Smith](https://smith.st).
+
+The goal is to provide a convenient interface for working with smith.
+
+### Configuration
+
+The smith cli will source credentials configuration as follows:
+ - It will check for an environment provided API key in '$SMITH_JWK'.
+ - It will fall-back to looking for '$SMITH_HOME/credentials.json' if '$SMITH_HOME' is set.
+ - It will fall-back to looking for '$HOME/.smith/credentials.json'.
+
+The smith cli will source endpoint configuration as follows:
+ - It will check for an environment provided endpoint in '$SMITH_ENDPOINT'.
+ - It will fall-back to the public production endpoint 'https://api.smith.st'.
+
+
+### Stability
+
+This cli is new, and should have the disclaimers that normally comes
+with that. However, the command line aims to maintain compatibility
+unless there is a non-small issue that breaking compatibility will
+really address. Stable versions will always be available for
+downoad/install if you really need to lock things down.
+
+
+### Example
+
+Using your ssh-agent.
+```
+# using ssh agent
+eval $(ssh-agent)
+
+# issue a certificate for the muppets environment
+smith --environment muppets
+smith -e muppets
+```
+
+Using smith to start ssh-agent.
+```
+# start ssh-agent issue a certificate for the muppets environment
+eval $(smith --environment muppets)
+```
+
+Running a command with access to an agent configured with your certificate.
+```
+# start ssh-agent issue a certificate for the muppets environment
+smith --environment muppets -- ssh user@kermit
+smith --environment muppets -- rsync -aH www www@gonzo:/var/www
+```
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/main/smith.hs b/main/smith.hs
new file mode 100644
--- /dev/null
+++ b/main/smith.hs
@@ -0,0 +1,47 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import           Control.Applicative ((<|>), some)
+
+import qualified Options.Applicative as Options
+
+import qualified Smith.Cli.Command.Issue as Issue
+import           Smith.Cli.Data.Program (Program (..))
+import qualified Smith.Cli.Dispatch as Dispatch
+import qualified Smith.Cli.Error as Error
+import qualified Smith.Cli.Parser as Parser
+
+import qualified Smith.Client as Smith
+import           Smith.Client.Data.CertificateRequest (Principal (..))
+import           Smith.Client.Data.Environment (Environment (..))
+
+import qualified System.Exit as Exit
+
+
+data Command =
+    Command Environment [Principal] (Maybe Program)
+    deriving (Eq, Ord, Show)
+
+
+-- |
+-- The 'smith' executable, designed for smith users who
+-- want to request access to servers.
+--
+main :: IO ()
+main =
+  Dispatch.dispatch parser >>= \a ->
+    case a of
+      Command environment principals program -> do
+        smith <- Error.runOrFlailT Smith.renderSmithConfigureError $
+          Smith.configureT
+        Error.runOrFlailT Issue.renderIssueError $
+          Issue.issue smith environment principals program
+        Exit.exitSuccess
+
+
+-- FUTURE default to current user, not root?
+parser :: Options.Parser Command
+parser =
+  Command
+    <$> Parser.environment
+    <*> ((some Parser.principal) <|> pure [Principal "root"])
+    <*> Options.optional Parser.program
diff --git a/smith-cli.cabal b/smith-cli.cabal
new file mode 100644
--- /dev/null
+++ b/smith-cli.cabal
@@ -0,0 +1,56 @@
+name: smith-cli
+version: 0.0.1
+synopsis: Command line tool for <https://smith.st/ Smith>.
+homepage: https://github.com/smith-security/smith-cli
+license:  BSD3
+license-file: LICENSE
+author: Mark Hibberd
+maintainer: mth@smith.st
+copyright: (c) 2019, HotelKilo
+bug-reports: https://github.com/smith-security/smith-cli/issues
+category: Security
+build-type: Simple
+extra-source-files: ChangeLog.md, README.md
+cabal-version: >= 1.10
+description:
+  This is a command line tool for interacting with <https://smith.st Smith>.
+source-repository head
+  type:     git
+  location: git@github.com:smith-security/smith-cli.git
+
+executable smith
+  default-language: Haskell2010
+  main-is: ../main/smith.hs
+  hs-source-dirs: src
+  build-depends:
+      base >= 3 && < 5
+    , attoparsec >= 0.9 && < 0.14
+    , bytestring == 0.10.*
+    , base64-bytestring == 1.0.*
+    , cereal == 0.5.*
+    , crypto-pubkey-openssh == 0.2.*
+    , crypto-pubkey-types == 0.4.*
+    , directory == 1.*
+    , filepath == 1.*
+    , HsOpenSSL == 0.11.*
+    , network == 2.*
+    , openssh-protocol == 0.0.*
+    , optparse-applicative >= 0.11 && < 0.15
+    , smith-client == 0.0.*
+    , text == 1.*
+    , transformers >= 0.4 && < 0.6
+    , transformers-bifunctors == 0.*
+    , unix
+  other-modules:
+    Smith.Cli.Agent
+    Smith.Cli.Command.Issue
+    Smith.Cli.Configuration
+    Smith.Cli.Data.Program
+    Smith.Cli.Dispatch
+    Smith.Cli.Error
+    Smith.Cli.KeyPair
+    Smith.Cli.Parser
+  ghc-options:
+    -Wall
+    -O2
+    -threaded
diff --git a/src/Smith/Cli/Agent.hs b/src/Smith/Cli/Agent.hs
new file mode 100644
--- /dev/null
+++ b/src/Smith/Cli/Agent.hs
@@ -0,0 +1,189 @@
+-- |
+-- 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
diff --git a/src/Smith/Cli/Command/Issue.hs b/src/Smith/Cli/Command/Issue.hs
new file mode 100644
--- /dev/null
+++ b/src/Smith/Cli/Command/Issue.hs
@@ -0,0 +1,119 @@
+-- |
+-- Command for requesting a certificate.
+--
+{-# LANGUAGE OverloadedStrings #-}
+module Smith.Cli.Command.Issue (
+    -- * Entry point
+    issue
+
+    -- * Errors
+  , IssueError (..)
+  , renderIssueError
+  ) where
+
+import           Control.Monad.IO.Class (MonadIO (..))
+import           Control.Monad.Trans.Bifunctor (BifunctorTrans (..))
+import           Control.Monad.Trans.Except (ExceptT (..))
+
+import           Data.Foldable (for_)
+import           Data.Text (Text)
+import qualified Data.Text as Text
+
+import qualified Smith.Cli.Agent as Agent
+import           Smith.Cli.Data.Program (Program (..))
+import           Smith.Cli.KeyPair (Comment (..), EncodedRSAKeyPair (..))
+import qualified Smith.Cli.KeyPair as KeyPair
+
+import           Smith.Client (Smith (..))
+import qualified Smith.Client as Smith
+import           Smith.Client.Error (SmithError (..), ErrorCode (..))
+import           Smith.Client.Data.CertificateRequest (Principal (..), PublicKey (..), CertificateRequest (..))
+import           Smith.Client.Data.Environment (Environment (..))
+
+import qualified System.Posix.Process as Posix
+
+-- |
+-- Requests a certificate, registering it with ssh-agent if possible.
+--
+-- If an ssh-agent is not currently running, it is started with
+-- output to 'System.IO.stdout' suitable for use with eval.
+--
+-- If a program is provided, the program is executed with an
+-- ssh-agent configured.
+--
+issue :: Smith -> Environment -> [Principal] -> Maybe Program -> ExceptT IssueError IO ()
+issue smith environment principals program = do
+  agent <- liftIO Agent.connect >>= maybe (left AgentNotAvailableError) pure
+  keys <- liftIO KeyPair.newRSAKeyPair
+  encoded <- maybe (left KeyGenerationError) pure $
+    KeyPair.encodeRSAKeyPair (Comment . mconcat $ ["smith-", getEnvironment environment]) keys
+  certificate <- firstT SmithIssueError .
+    Smith.runRequestT smith . Smith.issue $
+      CertificateRequest
+        (PublicKey $ sshRSAPublicKey encoded)
+        principals
+        environment
+        Nothing
+  firstT IssueAddKeyError $
+    Agent.addKey agent certificate keys
+  liftIO $ for_ program exec
+
+exec :: Program -> IO a
+exec (Program command arguments) =
+  Posix.executeFile
+    (Text.unpack command)
+    True
+    (Text.unpack <$> arguments)
+    Nothing
+
+data IssueError =
+    SmithIssueError SmithError
+  | AgentNotAvailableError
+  | KeyGenerationError
+  | IssueAddKeyError Agent.AddKeyError
+    deriving (Eq, Show)
+
+renderIssueError :: IssueError -> Text
+renderIssueError e =
+  case e of
+    SmithIssueError err ->
+      renderSmithError err
+    AgentNotAvailableError ->
+      "ssh-agent was not available, ensure it is running and SSH_AUTH_SOCK is set."
+    KeyGenerationError ->
+      "We couldn't generate a key-pair using OpenSSL for this access request, this is very unusual, try again and please raise a support issue if this persists."
+    IssueAddKeyError Agent.IncompleteKeyPair ->
+      "An invalid key-pair has been generated (via OpenSSL), this very unusual, try again and please raise a support issue if this persists."
+    IssueAddKeyError Agent.InvalidCertificate ->
+      "Smith has generated an invalid certificate, please ensure you are running a compatible client version. If running the correct client version please raise a support issue."
+    IssueAddKeyError Agent.CouldNotAddKey ->
+      "Smith has generated a key but could not add it to your ssh-agent. Please raise a support issue, and include your ssh-agent implementation and version."
+    IssueAddKeyError Agent.CouldNotAddCertificate ->
+      "Smith has generated a certificate but could not add it to your ssh-agent. Please raise a support issue, and include your ssh-agent implementation and version. Note that if you are using the gnome ssh-agent instead of openssh, it does not support certificates."
+    IssueAddKeyError (Agent.AddKeyProtocolError _) ->
+      "Smith had a problem talking to your ssh-agent and could not add key or certificate. Please raise a support issue, and include your ssh-agent implementation and version."
+
+renderSmithError :: SmithError -> Text
+renderSmithError e =
+  case e of
+    -- FUTURE: debug mode that prints message.
+    -- FUTURE: Handle specific error codes for better error messages.
+    SmithApplicationError code _message ->
+      mconcat ["There was an error performing your request [", getErrorCode code, "]."]
+    -- FUTURE: debug mode that prints message.
+    SmithAuthorizationError code _message ->
+      mconcat ["You are not authorized to perform this request [", getErrorCode code, "]."]
+    SmithAuthenticationError _err ->
+      mconcat ["Smith could not authenticate you, please check your credentials and connectivity to Smith."]
+    -- FUTURE: debug mode that prints body + message
+    SmithResponseParseError code _body _message ->
+      mconcat ["Smith response parse error [", Text.pack . show $ code, "]. Please check connectivity to Smith and retry request."]
+    -- FUTURE: debug mode that prints body.
+    SmithStatusCodeError code _body ->
+      mconcat ["Smith status code error [", Text.pack . show $ code, "]. Please check connectivity to Smith and retry request."]
+    SmithUrlParseError message ->
+      mconcat ["Smith client url-parse error [", message, "]. Check you are running the latest client version, and raise a supportissue if this issue persists ."]
+
+left :: Applicative m => x -> ExceptT x m a
+left =
+  ExceptT . pure . Left
diff --git a/src/Smith/Cli/Configuration.hs b/src/Smith/Cli/Configuration.hs
new file mode 100644
--- /dev/null
+++ b/src/Smith/Cli/Configuration.hs
@@ -0,0 +1,32 @@
+-- |
+-- Command line configuration.
+--
+{-# LANGUAGE OverloadedStrings #-}
+module Smith.Cli.Configuration (
+    Configuration (..)
+  , configure
+  ) where
+
+import qualified System.Directory as Directory
+import qualified System.Environment as Environment
+import           System.FilePath (FilePath, (</>))
+
+
+data Configuration =
+  Configuration {
+      configurationSmithHome :: FilePath
+    } deriving (Eq, Ord, Show)
+
+
+configure :: IO Configuration
+configure = do
+  Environment.lookupEnv "SMITH_HOME" >>=
+    maybe
+      (Configuration <$> defaultSmithHome)
+      (pure . Configuration)
+
+
+defaultSmithHome :: IO FilePath
+defaultSmithHome =
+  Directory.getHomeDirectory >>= \home ->
+    pure (home </> ".smith")
diff --git a/src/Smith/Cli/Data/Program.hs b/src/Smith/Cli/Data/Program.hs
new file mode 100644
--- /dev/null
+++ b/src/Smith/Cli/Data/Program.hs
@@ -0,0 +1,12 @@
+-- |
+-- Data types respresenting a program to execute.
+--
+module Smith.Cli.Data.Program (
+    Program (..)
+  ) where
+
+import           Data.Text (Text)
+
+data Program =
+    Program Text [Text]
+    deriving (Eq, Ord, Show)
diff --git a/src/Smith/Cli/Dispatch.hs b/src/Smith/Cli/Dispatch.hs
new file mode 100644
--- /dev/null
+++ b/src/Smith/Cli/Dispatch.hs
@@ -0,0 +1,33 @@
+-- |
+-- 'Options.Applicative' based dispatch code.
+--
+-- /danger:/ This module is designed to call System.Exit
+-- and as such should only be called from the main module.
+--
+module Smith.Cli.Dispatch (
+    dispatch
+  ) where
+
+import           Options.Applicative ((<**>))
+import qualified Options.Applicative as Options
+
+
+-- |
+-- Runs the provided command line parser with
+-- sensible defaults.
+--
+-- On parser failure, prints error message, usage and
+-- exits with a status code of @1@.
+--
+-- On parser success, the value will be returned.
+--
+dispatch :: Options.Parser a -> IO a
+dispatch parser = do
+  Options.customExecParser
+    (Options.prefs . mconcat $ [
+        Options.showHelpOnEmpty
+      , Options.showHelpOnError
+      ])
+    (Options.info
+      (parser <**> Options.helper)
+      Options.idm)
diff --git a/src/Smith/Cli/Error.hs b/src/Smith/Cli/Error.hs
new file mode 100644
--- /dev/null
+++ b/src/Smith/Cli/Error.hs
@@ -0,0 +1,53 @@
+-- |
+-- Command line error handling routines.
+--
+-- /danger:/ This module is designed to call System.Exit
+-- and as such should only be called from the main module.
+--
+module Smith.Cli.Error (
+    flail
+  , runOrFlailT
+  , runOrFlail
+  ) where
+
+import           Control.Monad.Trans.Except (ExceptT (..), runExceptT)
+
+import           Data.Text (Text)
+import qualified Data.Text.IO as Text
+
+import qualified System.Exit as Exit
+import qualified System.IO as IO
+
+
+-- |
+-- On left case, the handler will be used to print error
+-- message to 'System.IO.stderr' and exit with a status
+-- code of @1@.
+--
+-- On right case, the value will be returned.
+--
+runOrFlailT :: (e -> Text) -> ExceptT e IO a -> IO a
+runOrFlailT handler =
+  runOrFlail handler . runExceptT
+
+
+-- |
+-- On left case, the handler will be used to print error
+-- message to 'System.IO.stderr' and exit with a status
+-- code of @1@.
+--
+-- On right case, the value will be returned.
+--
+runOrFlail :: (e -> Text) -> IO (Either e a) -> IO a
+runOrFlail handler action =
+  action >>= either (flail . handler) pure
+
+
+-- |
+-- Print error message to 'System.IO.stderr' and exit with
+-- a status code of @1@.
+--
+flail :: Text -> IO a
+flail msg = do
+  Text.hPutStrLn IO.stderr msg
+  Exit.exitFailure
diff --git a/src/Smith/Cli/KeyPair.hs b/src/Smith/Cli/KeyPair.hs
new file mode 100644
--- /dev/null
+++ b/src/Smith/Cli/KeyPair.hs
@@ -0,0 +1,65 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Smith.Cli.KeyPair (
+    EncodedRSAKeyPair (..)
+  , Comment (..)
+  , newRSAKeyPair
+  , encodeRSAKeyPair
+  ) where
+
+import qualified Crypto.PubKey.OpenSsh as OpenSsh
+import qualified Crypto.Types.PubKey.RSA as RSA
+
+import           Data.Text (Text)
+import qualified Data.Text.Encoding as Text
+
+import qualified OpenSSL.RSA as OpenSSL
+
+import           System.IO (IO)
+
+
+newtype Comment =
+  Comment {
+      getComment :: Text
+    } deriving (Eq, Ord, Show)
+
+data EncodedRSAKeyPair =
+  EncodedRSAKeyPair {
+      sshRSAPublicKey :: Text
+    , sshRSAPrivateKey :: Text
+    } deriving (Eq, Ord, Show)
+
+-- RSA exponent matches ssh-keygen, see OpenSSL constant RSA_F4.
+-- https://github.com/openssh/openssh-portable/blob/7d68e262944c1fff1574600fe0e5e92ec8b398f5/sshkey.c#L1518
+newRSAKeyPair :: IO OpenSSL.RSAKeyPair
+newRSAKeyPair =
+  OpenSSL.generateRSAKey'
+    4096
+    (0x10001 {- OpenSSL.RSA_F4 = 65537 = 0x10001 -})
+
+encodeRSAKeyPair :: Comment -> OpenSSL.RSAKeyPair -> Maybe EncodedRSAKeyPair
+encodeRSAKeyPair comment openssl =
+  let
+    public =
+      RSA.PublicKey {
+          RSA.public_size = OpenSSL.rsaSize openssl
+        , RSA.public_n = OpenSSL.rsaN openssl
+        , RSA.public_e = OpenSSL.rsaE openssl
+        }
+    mprivate = do
+      dP <- OpenSSL.rsaDMP1 openssl
+      dQ <- OpenSSL.rsaDMQ1 openssl
+      qinv <- OpenSSL.rsaIQMP openssl
+      pure $ RSA.PrivateKey {
+          RSA.private_pub = public
+        , RSA.private_d = OpenSSL.rsaD openssl
+        , RSA.private_p = OpenSSL.rsaP openssl
+        , RSA.private_q = OpenSSL.rsaQ openssl
+        , RSA.private_dP = dP
+        , RSA.private_dQ = dQ
+        , RSA.private_qinv = qinv
+        }
+  in
+    flip fmap mprivate $ \private ->
+      EncodedRSAKeyPair
+        (Text.decodeUtf8 . OpenSsh.encodePublic . OpenSsh.OpenSshPublicKeyRsa public . Text.encodeUtf8 . getComment $ comment)
+        (Text.decodeUtf8 . OpenSsh.encodePrivate $ OpenSsh.OpenSshPrivateKeyRsa private)
diff --git a/src/Smith/Cli/Parser.hs b/src/Smith/Cli/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/Smith/Cli/Parser.hs
@@ -0,0 +1,64 @@
+-- |
+-- 'Options.Applicative' based parsers for use within
+-- the command line tools. This are centralised to
+-- help with consistency across various commands.
+--
+-- This module is designed to be import qualified.
+--
+-- > import qualified Smith.Cli.Parser as Parser
+--
+module Smith.Cli.Parser (
+  -- * flags
+    environment
+  , principal
+
+  -- * arguments
+  , program
+  ) where
+
+import           Control.Applicative (many)
+
+import           Data.Text (Text)
+import qualified Data.Text as Text
+
+import qualified Options.Applicative as Options
+
+import           Smith.Cli.Data.Program (Program (..))
+import           Smith.Client.Data.Environment (Environment (..))
+import           Smith.Client.Data.CertificateRequest (Principal (..))
+
+environment :: Options.Parser Environment
+environment =
+ fmap Environment . fmap Text.pack . Options.strOption . mconcat $ [
+      Options.long "environment"
+    , Options.short 'e'
+    , Options.metavar "ENVIRONMENT"
+    ]
+
+-- CONSIDER: 'user' alias or similar.
+principal :: Options.Parser Principal
+principal =
+ fmap Principal . fmap Text.pack . Options.strOption . mconcat $ [
+      Options.long "principal"
+    , Options.short 'p'
+    , Options.metavar "PRINCIPAL"
+    ]
+
+program :: Options.Parser Program
+program =
+  Program
+    <$> programName
+    <*> many programArgument
+
+programName :: Options.Parser Text
+programName =
+  fmap Text.pack . Options.strArgument . mconcat $ [
+      Options.metavar "PROGRAM"
+    , Options.help "Program to execute."
+    ]
+programArgument :: Options.Parser Text
+programArgument =
+ fmap Text.pack . Options.strArgument . mconcat $ [
+      Options.metavar "ARGUMENT"
+    , Options.help "Argument to provided program."
+    ]
