From f678145637ba6f42c36d07c19f8c764e5d537f72 Mon Sep 17 00:00:00 2001 From: Julien Tanguy Date: Thu, 9 Apr 2015 01:12:36 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + LICENSE | 30 ++++++++ README.md | 18 +++++ Setup.hs | 2 + bench/bench.hs | 35 +++++++++ hmacaroons.cabal | 63 +++++++++++++++++ src/Crypto/Macaroon.hs | 121 ++++++++++++++++++++++++++++++++ src/Crypto/Macaroon/Binder.hs | 28 ++++++++ src/Crypto/Macaroon/Internal.hs | 88 +++++++++++++++++++++++ test/Crypto/Macaroon/Tests.hs | 74 +++++++++++++++++++ test/tests.hs | 66 +++++++++++++++++ 11 files changed, 528 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Setup.hs create mode 100644 bench/bench.hs create mode 100644 hmacaroons.cabal create mode 100644 src/Crypto/Macaroon.hs create mode 100644 src/Crypto/Macaroon/Binder.hs create mode 100644 src/Crypto/Macaroon/Internal.hs create mode 100644 test/Crypto/Macaroon/Tests.hs create mode 100644 test/tests.hs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05d4d64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.cabal-sandbox/ +cabal.sandbox.config +dist/ diff --git a/LICENSE b/LICENSE new file mode 100644 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 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 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 index 0000000..e66dadd --- /dev/null +++ b/bench/bench.hs @@ -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 index 0000000..a9f6ea5 --- /dev/null +++ b/hmacaroons.cabal @@ -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 index 0000000..819a9eb --- /dev/null +++ b/src/Crypto/Macaroon.hs @@ -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 +- Time for better security in NoSQL + +-} +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 index 0000000..3ec3d67 --- /dev/null +++ b/src/Crypto/Macaroon/Binder.hs @@ -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 index 0000000..fc50486 --- /dev/null +++ b/src/Crypto/Macaroon/Internal.hs @@ -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 index 0000000..cdfb620 --- /dev/null +++ b/test/Crypto/Macaroon/Tests.hs @@ -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: + +-} +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 index 0000000..ba5dafd --- /dev/null +++ b/test/tests.hs @@ -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 + -- 2.41.0