]> git.immae.eu Git - github/fretlink/hmacaroons.git/commitdiff
Initial commit
authorJulien Tanguy <julien.tanguy@jhome.fr>
Wed, 8 Apr 2015 23:12:36 +0000 (01:12 +0200)
committerJulien Tanguy <julien.tanguy@jhome.fr>
Wed, 8 Apr 2015 23:12:36 +0000 (01:12 +0200)
.gitignore [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
Setup.hs [new file with mode: 0644]
bench/bench.hs [new file with mode: 0644]
hmacaroons.cabal [new file with mode: 0644]
src/Crypto/Macaroon.hs [new file with mode: 0644]
src/Crypto/Macaroon/Binder.hs [new file with mode: 0644]
src/Crypto/Macaroon/Internal.hs [new file with mode: 0644]
test/Crypto/Macaroon/Tests.hs [new file with mode: 0644]
test/tests.hs [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..05d4d64
--- /dev/null
@@ -0,0 +1,3 @@
+.cabal-sandbox/
+cabal.sandbox.config
+dist/
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..787874f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2015, Julien Tanguy
+
+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 Julien Tanguy 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 (file)
index 0000000..f3fd91a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,18 @@
+Macaroons: Pure haskell implementation of macaroons
+===================================================
+
+Macaroons is a pure haskell implementation of macaroons. It aims to provide
+compatibility at a serialized level with the [reference implementation](https://github.com/rescrv/libmacaroons)
+and the [python implementation](https://github.com/ecordell/pymacaroons)
+
+[Google paper on macaroons](http://research.google.com/pubs/pub41892.html)
+[Macaroons at Mozilla](https://air.mozilla.org/macaroons-cookies-with-contextual-caveats-for-decentralized-authorization-in-the-cloud/)
+[Time for better security in NoSQL](http://hackingdistributed.com/2014/11/23/macaroons-in-hyperdex/)
+[Pure java implementation](https://github.com/nitram509/jmacaroons)
+
+TODO
+====
+
+- Verifiy Macaroons
+- Discharge Macaroons
+
diff --git a/Setup.hs b/Setup.hs
new file mode 100644 (file)
index 0000000..9a994af
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/bench/bench.hs b/bench/bench.hs
new file mode 100644 (file)
index 0000000..e66dadd
--- /dev/null
@@ -0,0 +1,35 @@
+{-#LANGUAGE OverloadedStrings #-}
+
+import Data.ByteString (ByteString)
+import Criterion.Main
+
+import Crypto.Macaroon
+import Crypto.Macaroon.Internal
+
+
+loc :: ByteString
+loc = "http://mybank/"
+
+ident :: ByteString
+ident = "we used our secret key"
+
+key :: ByteString
+key = "this is our super secret key; only we should know it"
+
+cav :: ByteString
+cav = "test = caveat"
+
+
+{-#INLINE benchCreate#-}
+benchCreate :: (Key, Key, Location) -> Macaroon
+benchCreate (secret, ident, loc) = create secret ident loc
+
+{-#INLINE benchMint #-}
+benchMint :: ((Key, Key, Location), ByteString) -> Macaroon
+benchMint (ms,c) = addFirstPartyCaveat c (benchCreate ms)
+
+main = defaultMain [
+    bgroup "Crypto.Macaroon" [ bench "create"  $ nf benchCreate (key,ident,loc)
+                             , bench "mint"  $ nf benchMint ((key,ident,loc),cav)
+                             ]
+    ]
diff --git a/hmacaroons.cabal b/hmacaroons.cabal
new file mode 100644 (file)
index 0000000..a9f6ea5
--- /dev/null
@@ -0,0 +1,63 @@
+name:                hmacaroons
+version:             0.1.0.0
+synopsis:            Haskell implementation of macaroons
+-- description:         
+license:             BSD3
+license-file:        LICENSE
+author:              Julien Tanguy
+maintainer:          julien.tanguy@jhome.fr
+-- copyright:           
+category:            Data
+build-type:          Simple
+extra-source-files:  README.md
+cabal-version:       >=1.10
+
+library
+  exposed-modules:     Crypto.Macaroon,
+                       Crypto.Macaroon.Binder
+  other-modules:       Crypto.Macaroon.Internal
+  -- other-extensions:    
+  build-depends:  base >=4 && < 5,
+                  bytestring >=0.10,
+                  base64-bytestring >= 1.0,
+                  byteable >= 0.1 && <0.2,
+                  cereal >= 0.4,
+                  cryptohash >=0.11 && <0.12,
+                  cipher-aes >=0.2 && <0.3,
+                  deepseq >= 1.1,
+                  hex >= 0.1
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+benchmark bench
+  default-language: Haskell2010
+  type:             exitcode-stdio-1.0
+  hs-source-dirs:   src, bench
+  main-is:          bench.hs
+  ghc-options:      -O2
+  build-depends:  base >= 4 && <5,
+                  bytestring >=0.10,
+                  base64-bytestring >= 1.0,
+                  cereal >= 0.4,
+                  cryptohash >=0.11 && <0.12,
+                  cipher-aes >=0.2 && <0.3,
+                  byteable >= 0.1 && <0.2,
+                  hex >= 0.1,
+                  deepseq >= 1.1,
+                  criterion >= 1.1
+test-suite test
+  default-language: Haskell2010
+  type: exitcode-stdio-1.0
+  hs-source-dirs: test
+  main-is: tests.hs
+  build-depends:  base >= 4 && <5,
+                  bytestring >=0.10,
+                  base64-bytestring >= 1.0,
+                  byteable >= 0.1 && <0.2,
+                  cereal >= 0.4,
+                  cryptohash >=0.11 && <0.12,
+                  hex >= 0.1,
+                  tasty >= 0.10,
+                  tasty-hunit >= 0.9,
+                  hmacaroons
diff --git a/src/Crypto/Macaroon.hs b/src/Crypto/Macaroon.hs
new file mode 100644 (file)
index 0000000..819a9eb
--- /dev/null
@@ -0,0 +1,121 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-|
+Module      : Crypto.Macaroon
+Copyright   : (c) 2015 Julien Tanguy
+License     : BSD3
+
+Maintainer  : julien.tanguy@jhome.fr
+Stability   : experimental
+Portability : portable
+
+
+Pure haskell implementations of macaroons.
+
+Warning: this implementation has not been audited by security experts.
+Use it with caution.
+
+
+References:
+
+- Macaroons: Cookies with Contextual Caveats for Decentralized Authorization in the Cloud <http://research.google.com/pubs/pub41892.html>
+- Time for better security in NoSQL <http://hackingdistributed.com/2014/11/23/macaroons-in-hyperdex>
+
+-}
+module Crypto.Macaroon (
+    -- * Types
+      Macaroon
+    , Caveat
+    , Key
+    , Location
+    -- * Accessing functions
+    -- ** Macaroons
+    , location
+    , identifier
+    , caveats
+    , signature
+    -- ** Caveats
+    , caveatLoc
+    , caveatId
+    , caveatVId
+
+    -- * Create Macaroons
+    , create
+    , inspect
+    , addFirstPartyCaveat
+    , addThirdPartyCaveat
+
+    -- * Prepare Macaroons for transfer
+    , serialize
+    ) where
+
+import           Crypto.Cipher.AES
+import           Crypto.Hash
+import           Data.Byteable
+import qualified Data.ByteString            as BS
+import qualified Data.ByteString.Base64.URL as B64
+import qualified Data.ByteString.Char8      as B8
+import           Data.Hex
+
+import           Crypto.Macaroon.Internal
+
+-- | Create a Macaroon from its key, identifier and location
+create :: Key -> Key -> Location -> Macaroon
+create secret ident loc = MkMacaroon loc ident [] (toBytes (hmac derivedKey ident :: HMAC SHA256))
+  where
+    derivedKey = toBytes $ (hmac "macaroons-key-generator" secret :: HMAC SHA256)
+
+caveatLoc :: Caveat -> Location
+caveatLoc = cl
+
+caveatId :: Caveat -> Key
+caveatId = cid
+
+caveatVId :: Caveat -> Key
+caveatVId = vid
+
+inspect :: Macaroon -> String
+inspect m = unlines [ "location " ++ show (location m)
+                    , "identifier " ++ show (identifier m)
+                    , (concatMap (showCav (location m)) (caveats m))
+                    , "signature " ++ show (hex $ signature m)
+                    ]
+  where
+    showCav loc c | cl c == loc && vid c == BS.empty = "cid " ++ show (cid c)
+                  | otherwise = unlines [ "cid " ++ show (cid c)
+                                        , "vid " ++ show (vid c)
+                                        , "cl " ++ show (cl c)
+                                        ]
+
+serialize :: Macaroon -> BS.ByteString
+serialize m = B8.filter (/= '=') . B64.encode $ packets
+  where
+    packets = BS.concat [ putPacket "location" (location m)
+                        , putPacket "identifier" (identifier m)
+                        , caveatPackets
+                        , putPacket "signature" (signature m)
+                        ]
+    caveatPackets = BS.concat $ map (cavPacket (location m)) (caveats m)
+    cavPacket loc c | cl c == loc && vid c == BS.empty = putPacket "cid" (cid c)
+                    | otherwise = BS.concat [ putPacket "cid" (cid c)
+                                            , putPacket "vid" (vid c)
+                                            , putPacket "cl" (cl c)
+                                            ]
+
+
+
+-- | Add a first party Caveat to a Macaroon, with its identifier
+addFirstPartyCaveat :: Key -> Macaroon -> Macaroon
+addFirstPartyCaveat ident m = addCaveat (location m) ident BS.empty m
+
+-- |Add a third party Caveat to a Macaroon, using its location, identifier and
+-- verification key
+addThirdPartyCaveat :: Key
+                    -> Key
+                    -> Location
+                    -> Macaroon
+                    -> Macaroon
+addThirdPartyCaveat key cid loc m = addCaveat loc cid vid m
+  where
+    vid = encryptECB (initAES (signature m)) key
+
+
diff --git a/src/Crypto/Macaroon/Binder.hs b/src/Crypto/Macaroon/Binder.hs
new file mode 100644 (file)
index 0000000..3ec3d67
--- /dev/null
@@ -0,0 +1,28 @@
+{-|
+Module      : Crypto.Macaroon.Binder
+Copyright   : (c) 2015 Julien Tanguy
+License     : BSD3
+
+Maintainer  : julien.tanguy@jhome.fr
+Stability   : experimental
+Portability : portable
+
+
+
+-}
+module Crypto.Macaroon.Binder where
+
+import           Crypto.Hash
+import           Data.Byteable
+import qualified Data.ByteString          as BS
+
+import           Crypto.Macaroon.Internal
+
+-- | Datatype for binding discharging and authorizing macaroons together
+newtype Binder = Binder { bind :: Macaroon -> Macaroon -> BS.ByteString }
+
+
+-- | Binder which concatenates the two signatures and hashes them
+hashSigs :: Binder
+hashSigs = Binder $ \m m' -> toBytes $ (HMAC . hash $ BS.append (toBytes $ signature m') (toBytes $ signature m) :: HMAC SHA256)
+
diff --git a/src/Crypto/Macaroon/Internal.hs b/src/Crypto/Macaroon/Internal.hs
new file mode 100644 (file)
index 0000000..fc50486
--- /dev/null
@@ -0,0 +1,88 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-|
+Module      : Crypto.Macaroon.Internal
+Copyright   : (c) 2015 Julien Tanguy
+License     : BSD3
+
+Maintainer  : julien.tanguy@jhome.fr
+Stability   : experimental
+Portability : portable
+
+
+Internal representation of a macaroon
+-}
+module Crypto.Macaroon.Internal where
+
+
+import           Control.DeepSeq
+import           Crypto.Cipher.AES
+import           Crypto.Hash
+import           Data.Byteable
+import qualified Data.ByteString        as BS
+import qualified Data.ByteString.Base64 as B64
+import qualified Data.ByteString.Char8  as B8
+import           Data.Char
+import           Data.Hex
+import           Data.Serialize
+import           Data.Word
+
+-- |Type alias for Macaroons and Caveat keys and identifiers
+type Key = BS.ByteString
+
+-- |Type alias For Macaroons and Caveat locations
+type Location = BS.ByteString
+
+type Sig = BS.ByteString
+
+-- | Main structure of a macaroon
+data Macaroon = MkMacaroon { location   :: Location
+                           -- ^ Target location
+                           , identifier :: Key
+                           -- ^ Macaroon Identifier
+                           , caveats    :: [Caveat]
+                           -- ^ List of caveats
+                           , signature  :: Sig
+                           -- ^ Macaroon HMAC signature
+                           } deriving (Eq)
+
+
+instance NFData Macaroon where
+    rnf (MkMacaroon loc ident cavs sig) = rnf loc `seq` rnf ident `seq` rnf cavs `seq` rnf sig
+
+
+-- | Caveat structure
+data Caveat = MkCaveat { cid :: Key
+                       -- ^ Caveat identifier
+                       , vid :: Key
+                       -- ^ Caveat verification key identifier
+                       , cl  :: Location
+                       -- ^ Caveat target location
+
+                       } deriving (Eq)
+
+instance NFData Caveat where
+    rnf (MkCaveat cid vid cl) = rnf cid `seq` rnf vid `seq` rnf cl
+
+
+putPacket :: BS.ByteString -> BS.ByteString -> BS.ByteString
+putPacket key dat = BS.concat [
+    B8.map toLower . hex . encode $ (fromIntegral size :: Word16)
+    , key
+    , " "
+    , dat
+    , "\n"
+    ]
+  where
+    size = 4 + 2 + BS.length key + BS.length dat
+
+addCaveat :: Location
+          -> Key
+          -> Key
+          -> Macaroon
+          -> Macaroon
+addCaveat loc cid vid m = m { caveats = cavs ++ [cav'], signature = sig}
+  where
+    cavs = caveats m
+    cav' = MkCaveat cid vid loc
+    sig = toBytes $ (hmac (signature m) (BS.append vid cid) :: HMAC SHA256)
+
diff --git a/test/Crypto/Macaroon/Tests.hs b/test/Crypto/Macaroon/Tests.hs
new file mode 100644 (file)
index 0000000..cdfb620
--- /dev/null
@@ -0,0 +1,74 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-|
+Copyright   : (c) 2015 Julien Tanguy
+License     : BSD3
+
+Maintainer  : julien.tanguy@jhome.fr
+
+
+This test suite is based on the pymacaroons test suite:
+<https://github.com/ecordell/pymacaroons>
+-}
+module Crypto.Macaroon.Tests where
+
+import           Data.Byteable
+import qualified Data.ByteString.Char8 as B8
+import           Data.Hex
+import           Test.Tasty
+import           Test.Tasty.HUnit
+
+import           Crypto.Macaroon
+
+tests :: TestTree
+tests = testGroup "Crypto.Macaroon" [ basicSignature
+                                    , basicSerialize
+                                    , basicMint
+                                    , basicMintTrimmed
+                                    ]
+
+
+m :: Macaroon
+m = create secret key loc
+  where
+    secret = B8.pack "this is our super secret key; only we should know it"
+    key = B8.pack "we used our secret key"
+    loc = B8.pack "http://mybank/"
+
+m2 :: Macaroon
+m2 = addFirstPartyCaveat "test = caveat" m
+
+m3 :: Macaroon
+m3 = addFirstPartyCaveat "test = acaveat" m
+
+m4 :: Macaroon
+m4 = addThirdPartyCaveat caveat_key caveat_id caveat_loc n
+  where
+    n = addFirstPartyCaveat "account = 3735928559" $ create sec key loc
+    key = B8.pack "we used our other secret key"
+    loc = B8.pack "http://mybank/"
+    sec = B8.pack "this is a different super-secret key; never use the same secret twice"
+    caveat_key = B8.pack "4; guaranteed random by a fair toss of the dice"
+    caveat_id = B8.pack "this was how we remind auth of key/pred"
+    caveat_loc = B8.pack "http://auth.mybank/"
+
+
+basicSignature = testCase "Basic signature" $
+    "E3D9E02908526C4C0039AE15114115D97FDD68BF2BA379B342AAF0F617D0552F" @=? (hex . signature) m
+
+basicSerialize = testCase "Serialization" $
+    "MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudG\
+    \lmaWVyIHdlIHVzZWQgb3VyIHNlY3JldCBrZXkKMDAyZnNpZ25h\
+    \dHVyZSDj2eApCFJsTAA5rhURQRXZf91ovyujebNCqvD2F9BVLwo" @=? serialize m
+
+basicMint = testCase "First Party Caveat" $
+    "MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudGlmaWVyIHdlIHVzZ\
+    \WQgb3VyIHNlY3JldCBrZXkKMDAxNmNpZCB0ZXN0ID0gY2F2ZWF0CjAwMmZzaWduYXR1cmUgGXusegR\
+    \K8zMyhluSZuJtSTvdZopmDkTYjOGpmMI9vWcK" @=? serialize m2
+
+basicMintTrimmed = testCase "Trimmed base64" $ 
+    "MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudGlmaWVyIHdlIHVz\
+    \ZWQgb3VyIHNlY3JldCBrZXkKMDAxN2NpZCB0ZXN0ID0gYWNhdmVhdAowMDJmc2ln\
+    \bmF0dXJlIJRJ_V3WNJQnqlVq5eez7spnltwU_AXs8NIRY739sHooCg" @=? serialize m3
+
+basicThirdParty = testCase "Third Party Caveat" $ 
+    "6B99EDB2EC6D7A4382071D7D41A0BF7DFA27D87D2F9FEA86E330D7850FFDA2B2" @=? (hex . signature) m4
diff --git a/test/tests.hs b/test/tests.hs
new file mode 100644 (file)
index 0000000..ba5dafd
--- /dev/null
@@ -0,0 +1,66 @@
+{-#LANGUAGE OverloadedStrings#-}
+
+import           Crypto.Hash
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as B
+import Data.Hex
+import Data.Byteable
+
+import Test.Tasty
+import Test.Tasty.HUnit
+
+import qualified Crypto.Macaroon.Tests
+
+main = defaultMain tests
+
+tests :: TestTree
+tests = testGroup "Tests" [ sanityCheck
+                          , Crypto.Macaroon.Tests.tests
+                          ]
+
+sanityCheck :: TestTree
+sanityCheck = testGroup "Python HMAC Sanity check" [ checkKey
+    , checkMac1
+    , checkMac2
+    , checkMac3
+    , checkMac4
+    ]
+
+
+secret :: ByteString
+secret = "this is our super secret key; only we should know it"
+
+public :: ByteString
+public = "we used our secret key"
+
+key :: ByteString
+key = B.take 32 secret
+
+mac1 :: ByteString
+mac1 = toBytes $ (hmac key public :: HMAC SHA256)
+
+mac2 :: ByteString
+mac2 = toBytes $ (hmac mac1 "account = 3735928559" :: HMAC SHA256)
+
+mac3 :: ByteString
+mac3 = toBytes $ (hmac mac2 "time < 2015-01-01T00:00" :: HMAC SHA256)
+
+mac4 :: ByteString
+mac4 = toBytes $ (hmac mac3 "email = alice@example.org" :: HMAC SHA256)
+
+
+checkKey = testCase "Truncated key" $ 
+    key @?= "this is our super secret key; on"
+
+checkMac1 = testCase "HMAC key" $ 
+    "C60B4B3540BB1B2F2EF28D1C895691CC4A5E07A38A9D3B1C3379FB485293372F" @=? hex mac1
+
+checkMac2 = testCase "HMAC key account" $ 
+    "5C933DC9A7D036DFCD1740B4F26D737397A1FF635EAC900F3226973503CAAAA5" @=? hex mac2
+
+checkMac3 = testCase "HMAC key account time" $ 
+    "7A559B20C8B607009EBCE138C200585E9D0DECA6D23B3EAD6C5E0BA6861D3858" @=? hex mac3
+
+checkMac4 = testCase "HMAC key account time email" $ 
+    "E42BBB02A9A5A303483CB6295C497AE51AD1D5CB10003CBE548D907E7E62F5E4" @=? hex mac4
+