diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Revision history for openssh-protocol
+
+## 0.0.1  -- 2019-01-27
+
+* First version. Extracted from internal libraries.
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,29 @@
+# openssh-protocol
+
+Haskell implementation of openssh protocol primitives.
+
+The openssh primitives are defined in [RFC4251](https://www.ietf.org/rfc/rfc4251.txt).
+
+They are used by various parts of the openssh toolchain:
+ - [ssh](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL)
+ - [certificates](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkey)
+ - [agent](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.agent)
+ - [certkeys](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys)
+ - [key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key)
+ - [krl](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.krl)
+ - [mux](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.mux)
+
+### Stability
+
+This library should be considered stable. Primitives will
+be added but will not removed.
+
+### Future
+
+Right now, these are the building blocks required to implement the
+higher level protocol components. They are useful, but the end goal
+is to produce public implementations of all the protocol,
+most importantly:
+ - Key pair encode/decode.
+ - Ceritificate encode/decode + signing.
+ - Agent message protocol.
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/openssh-protocol.cabal b/openssh-protocol.cabal
new file mode 100644
--- /dev/null
+++ b/openssh-protocol.cabal
@@ -0,0 +1,54 @@
+name: openssh-protocol
+version: 0.0.1
+synopsis: Haskell implementation of openssh protocol primitives.
+homepage: https://github.com/smith-security/openssh-protocol
+license:  BSD3
+license-file: LICENSE
+author: Mark Hibberd
+maintainer: mth@smith.st
+copyright: (c) 2019, HotelKilo
+bug-reports: https://github.com/smith-security/openssh-protocol/issues
+category: Security
+build-type: Simple
+extra-source-files: ChangeLog.md, README.md
+cabal-version: >= 1.10
+description:
+  This is a library collecting openssh protocol primitives
+  using 'cereal'.
+source-repository head
+  type:     git
+  location: git@github.com:smith-security/openssh-protocol.git
+
+library
+  default-language: Haskell2010
+  build-depends:
+      base >= 4.10 && < 5
+    , bytestring == 0.10.*
+    , cereal == 0.5.*
+    , integer-logarithms == 1.*
+    , text == 1.*
+    , time == 1.*
+    , vector == 0.12.*
+
+  hs-source-dirs:
+    src
+
+  exposed-modules:
+    Crypto.OpenSSH.Protocol.Encode
+    Crypto.OpenSSH.Protocol.Decode
+
+
+test-suite test
+  default-language: Haskell2010
+  type: exitcode-stdio-1.0
+  main-is: test.hs
+  hs-source-dirs: test
+  build-depends:
+      base >= 4.10 && < 5
+    , cereal
+    , hedgehog == 0.6.*
+    , openssh-protocol
+    , time
+
+  other-modules:
+    Test.Crypto.OpenSSH.Protocol
diff --git a/src/Crypto/OpenSSH/Protocol/Decode.hs b/src/Crypto/OpenSSH/Protocol/Decode.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/OpenSSH/Protocol/Decode.hs
@@ -0,0 +1,59 @@
+-- |
+-- OpenSSH protocol primitives as defined by <RFC4251 https://www.ietf.org/rfc/rfc4251.txt>.
+--
+{-# LANGUAGE ScopedTypeVariables #-}
+module Crypto.OpenSSH.Protocol.Decode (
+    text
+  , string
+  , uint32
+  , uint64
+  , mpint
+  , time
+  , utc
+  ) where
+
+import           Data.Bits (Bits (..))
+import           Data.ByteString (ByteString)
+import qualified Data.Serialize as Serialize
+import           Data.Text (Text)
+import qualified Data.Text.Encoding as Text
+import           Data.Time (UTCTime)
+import           Data.Time.Clock.POSIX (POSIXTime)
+import qualified Data.Time.Clock.POSIX as Time
+import           Data.Word (Word32, Word64)
+import qualified Data.Vector.Primitive as Vector
+
+
+text :: Serialize.Get Text
+text =
+  Text.decodeUtf8 <$> string
+
+string :: Serialize.Get ByteString
+string =
+  uint32 >>= Serialize.getBytes . fromIntegral
+
+uint32 :: Serialize.Get Word32
+uint32 =
+  Serialize.getWord32be
+
+uint64 :: Serialize.Get Word64
+uint64 =
+  Serialize.getWord64be
+
+mpint :: Serialize.Get Integer
+mpint = do
+  size <- fromIntegral <$> uint32
+  bytes <- Vector.replicateM size Serialize.getWord8
+  let
+    value = Vector.foldl' (\acc el -> (acc `shiftL` 8) + fromIntegral el) (0 :: Integer) bytes
+    negative = size > 0 && testBit (Vector.head bytes) 7
+  pure $
+    if negative then value - 2 ^ (size * 8) else value
+
+time :: Serialize.Get POSIXTime
+time =
+  fromIntegral <$> uint64
+
+utc :: Serialize.Get UTCTime
+utc =
+  Time.posixSecondsToUTCTime <$> time
diff --git a/src/Crypto/OpenSSH/Protocol/Encode.hs b/src/Crypto/OpenSSH/Protocol/Encode.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/OpenSSH/Protocol/Encode.hs
@@ -0,0 +1,88 @@
+-- |
+-- OpenSSH protocol primitives as defined by <RFC4251 https://www.ietf.org/rfc/rfc4251.txt>.
+--
+module Crypto.OpenSSH.Protocol.Encode (
+    text
+  , string
+  , uint32
+  , uint64
+  , mpint
+  , time
+  , utc
+  ) where
+
+import           Data.Bits (Bits (..))
+import           Data.ByteString (ByteString)
+import qualified Data.ByteString as ByteString
+import qualified Data.Serialize as Serialize
+import           Data.Text (Text)
+import qualified Data.Text.Encoding as Text
+import           Data.Time (UTCTime)
+import           Data.Time.Clock.POSIX (POSIXTime)
+import qualified Data.Time.Clock.POSIX as Time
+import           Data.Word (Word8, Word32, Word64)
+import qualified Data.Vector.Primitive as Vector
+
+import qualified Math.NumberTheory.Logarithms as Logarithms
+
+
+text :: Text -> Serialize.Put
+text =
+  string . Text.encodeUtf8
+
+string :: ByteString -> Serialize.Put
+string bytes = do
+  uint32 . fromIntegral . ByteString.length $ bytes
+  Serialize.putByteString bytes
+
+uint32 :: Word32 -> Serialize.Put
+uint32 =
+  Serialize.putWord32be
+
+uint64 :: Word64 -> Serialize.Put
+uint64 =
+  Serialize.putWord64be
+
+mpint :: Integer -> Serialize.Put
+mpint n =
+  string . Serialize.runPut $
+    Vector.forM_ (expand n) $
+      Serialize.put
+
+time :: POSIXTime -> Serialize.Put
+time =
+  uint64 . floor . toRational
+
+utc :: UTCTime -> Serialize.Put
+utc =
+  uint64 . floor . toRational . Time.utcTimeToPOSIXSeconds
+
+expand :: Integer -> Vector.Vector Word8
+expand n =
+  let
+    base = expand' n
+  in
+    if n == 0 then
+      Vector.empty
+    else if n > 0 && testBit (Vector.head base) 7 then
+      Vector.cons 0 base
+    else if n < 0 && not (testBit (Vector.head base) 7) then
+      Vector.cons 0xff base
+    else
+      base
+
+expand' :: Integer -> Vector.Vector Word8
+expand' nn =
+  let
+    loop x n =
+      let
+        (n', w) = quotRem n 256
+      in
+        if n == 0 then x else loop (fromIntegral w : x) n'
+    bytes =
+      (Logarithms.integerLogBase 2 (abs nn) + 1) `quot` 8 + 1
+  in
+    if nn < 0 then
+      expand' $ 2 ^ (8 * bytes) + nn
+    else
+      Vector.fromList . loop [] $ nn
diff --git a/test/Test/Crypto/OpenSSH/Protocol.hs b/test/Test/Crypto/OpenSSH/Protocol.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Crypto/OpenSSH/Protocol.hs
@@ -0,0 +1,67 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Test.Crypto.OpenSSH.Protocol where
+
+import qualified Crypto.OpenSSH.Protocol.Encode as Encode
+import qualified Crypto.OpenSSH.Protocol.Decode as Decode
+
+import qualified Data.Serialize as Serialize
+import qualified Data.Time as Time
+
+import           Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+
+
+trip :: (Eq a, Show a) => (a -> Serialize.Put) -> Serialize.Get a -> Gen a -> PropertyT IO ()
+trip put get gen = do
+  a <- forAll gen
+  Right a === Serialize.runGet get (Serialize.runPut $ put a)
+
+prop_mpint :: Property
+prop_mpint =
+  withTests 10000 . property $ do
+    trip Encode.mpint Decode.mpint $
+      Gen.integral (Range.linearFrom 0 (-999999999999999999999999) 999999999999999999999999)
+
+prop_text :: Property
+prop_text =
+  withTests 10000 . property $ do
+    trip Encode.text Decode.text $
+      Gen.text (Range.linear 0 100) Gen.alphaNum
+
+prop_string :: Property
+prop_string =
+  withTests 10000 . property $ do
+    trip Encode.string Decode.string $
+      Gen.utf8 (Range.linear 0 100) Gen.alphaNum
+
+prop_uint32 :: Property
+prop_uint32 =
+  withTests 10000 . property $ do
+    trip Encode.uint32 Decode.uint32 $
+      Gen.integral Range.linearBounded
+
+prop_uint64 :: Property
+prop_uint64 =
+  withTests 10000 . property $ do
+    trip Encode.uint64 Decode.uint64 $
+      Gen.integral Range.linearBounded
+
+prop_time :: Property
+prop_time =
+  withTests 10000 . property $ do
+    trip Encode.time Decode.time $
+      fromIntegral <$> Gen.int (Range.linear 0 maxBound)
+
+prop_utc :: Property
+prop_utc =
+  withTests 10000 . property $ do
+    trip Encode.utc Decode.utc $ do
+      day <- Time.ModifiedJulianDay <$> Gen.integral (Range.linearFrom 50000 58500 90000)
+      time <- fromInteger <$> Gen.integral (Range.constant 0 86399)
+      pure $ Time.UTCTime day time
+
+tests :: IO Bool
+tests =
+  checkParallel $$(discover)
diff --git a/test/test.hs b/test/test.hs
new file mode 100644
--- /dev/null
+++ b/test/test.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+
+import           Control.Monad ((>>=), (>>), when, mapM)
+
+import           Prelude (($), (.), not, all, id)
+
+import qualified System.Exit as Exit
+import           System.IO (IO)
+import qualified System.IO as IO
+
+import qualified Test.Crypto.OpenSSH.Protocol
+
+main :: IO ()
+main =
+  IO.hSetBuffering IO.stdout IO.LineBuffering >> mapM id [
+      Test.Crypto.OpenSSH.Protocol.tests
+    ] >>= \rs -> when (not . all id $ rs) Exit.exitFailure
