aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--LICENSE30
-rw-r--r--README.md18
-rw-r--r--Setup.hs2
-rw-r--r--bench/bench.hs35
-rw-r--r--hmacaroons.cabal63
-rw-r--r--src/Crypto/Macaroon.hs121
-rw-r--r--src/Crypto/Macaroon/Binder.hs28
-rw-r--r--src/Crypto/Macaroon/Internal.hs88
-rw-r--r--test/Crypto/Macaroon/Tests.hs74
-rw-r--r--test/tests.hs66
11 files changed, 528 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..05d4d64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
1.cabal-sandbox/
2cabal.sandbox.config
3dist/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..787874f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
1Copyright (c) 2015, Julien Tanguy
2
3All rights reserved.
4
5Redistribution and use in source and binary forms, with or without
6modification, are permitted provided that the following conditions are met:
7
8 * Redistributions of source code must retain the above copyright
9 notice, this list of conditions and the following disclaimer.
10
11 * Redistributions in binary form must reproduce the above
12 copyright notice, this list of conditions and the following
13 disclaimer in the documentation and/or other materials provided
14 with the distribution.
15
16 * Neither the name of Julien Tanguy nor the names of other
17 contributors may be used to endorse or promote products derived
18 from this software without specific prior written permission.
19
20THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30OF 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 @@
1Macaroons: Pure haskell implementation of macaroons
2===================================================
3
4Macaroons is a pure haskell implementation of macaroons. It aims to provide
5compatibility at a serialized level with the [reference implementation](https://github.com/rescrv/libmacaroons)
6and the [python implementation](https://github.com/ecordell/pymacaroons)
7
8[Google paper on macaroons](http://research.google.com/pubs/pub41892.html)
9[Macaroons at Mozilla](https://air.mozilla.org/macaroons-cookies-with-contextual-caveats-for-decentralized-authorization-in-the-cloud/)
10[Time for better security in NoSQL](http://hackingdistributed.com/2014/11/23/macaroons-in-hyperdex/)
11[Pure java implementation](https://github.com/nitram509/jmacaroons)
12
13TODO
14====
15
16- Verifiy Macaroons
17- Discharge Macaroons
18
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
index 0000000..9a994af
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
1import Distribution.Simple
2main = 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 @@
1{-#LANGUAGE OverloadedStrings #-}
2
3import Data.ByteString (ByteString)
4import Criterion.Main
5
6import Crypto.Macaroon
7import Crypto.Macaroon.Internal
8
9
10loc :: ByteString
11loc = "http://mybank/"
12
13ident :: ByteString
14ident = "we used our secret key"
15
16key :: ByteString
17key = "this is our super secret key; only we should know it"
18
19cav :: ByteString
20cav = "test = caveat"
21
22
23{-#INLINE benchCreate#-}
24benchCreate :: (Key, Key, Location) -> Macaroon
25benchCreate (secret, ident, loc) = create secret ident loc
26
27{-#INLINE benchMint #-}
28benchMint :: ((Key, Key, Location), ByteString) -> Macaroon
29benchMint (ms,c) = addFirstPartyCaveat c (benchCreate ms)
30
31main = defaultMain [
32 bgroup "Crypto.Macaroon" [ bench "create" $ nf benchCreate (key,ident,loc)
33 , bench "mint" $ nf benchMint ((key,ident,loc),cav)
34 ]
35 ]
diff --git a/hmacaroons.cabal b/hmacaroons.cabal
new file mode 100644
index 0000000..a9f6ea5
--- /dev/null
+++ b/hmacaroons.cabal
@@ -0,0 +1,63 @@
1name: hmacaroons
2version: 0.1.0.0
3synopsis: Haskell implementation of macaroons
4-- description:
5license: BSD3
6license-file: LICENSE
7author: Julien Tanguy
8maintainer: julien.tanguy@jhome.fr
9-- copyright:
10category: Data
11build-type: Simple
12extra-source-files: README.md
13cabal-version: >=1.10
14
15library
16 exposed-modules: Crypto.Macaroon,
17 Crypto.Macaroon.Binder
18 other-modules: Crypto.Macaroon.Internal
19 -- other-extensions:
20 build-depends: base >=4 && < 5,
21 bytestring >=0.10,
22 base64-bytestring >= 1.0,
23 byteable >= 0.1 && <0.2,
24 cereal >= 0.4,
25 cryptohash >=0.11 && <0.12,
26 cipher-aes >=0.2 && <0.3,
27 deepseq >= 1.1,
28 hex >= 0.1
29 hs-source-dirs: src
30 default-language: Haskell2010
31
32benchmark bench
33 default-language: Haskell2010
34 type: exitcode-stdio-1.0
35 hs-source-dirs: src, bench
36 main-is: bench.hs
37 ghc-options: -O2
38 build-depends: base >= 4 && <5,
39 bytestring >=0.10,
40 base64-bytestring >= 1.0,
41 cereal >= 0.4,
42 cryptohash >=0.11 && <0.12,
43 cipher-aes >=0.2 && <0.3,
44 byteable >= 0.1 && <0.2,
45 hex >= 0.1,
46 deepseq >= 1.1,
47 criterion >= 1.1
48
49test-suite test
50 default-language: Haskell2010
51 type: exitcode-stdio-1.0
52 hs-source-dirs: test
53 main-is: tests.hs
54 build-depends: base >= 4 && <5,
55 bytestring >=0.10,
56 base64-bytestring >= 1.0,
57 byteable >= 0.1 && <0.2,
58 cereal >= 0.4,
59 cryptohash >=0.11 && <0.12,
60 hex >= 0.1,
61 tasty >= 0.10,
62 tasty-hunit >= 0.9,
63 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 @@
1{-# LANGUAGE OverloadedStrings #-}
2{-|
3Module : Crypto.Macaroon
4Copyright : (c) 2015 Julien Tanguy
5License : BSD3
6
7Maintainer : julien.tanguy@jhome.fr
8Stability : experimental
9Portability : portable
10
11
12Pure haskell implementations of macaroons.
13
14Warning: this implementation has not been audited by security experts.
15Use it with caution.
16
17
18References:
19
20- Macaroons: Cookies with Contextual Caveats for Decentralized Authorization in the Cloud <http://research.google.com/pubs/pub41892.html>
21- Time for better security in NoSQL <http://hackingdistributed.com/2014/11/23/macaroons-in-hyperdex>
22
23-}
24module Crypto.Macaroon (
25 -- * Types
26 Macaroon
27 , Caveat
28 , Key
29 , Location
30 -- * Accessing functions
31 -- ** Macaroons
32 , location
33 , identifier
34 , caveats
35 , signature
36 -- ** Caveats
37 , caveatLoc
38 , caveatId
39 , caveatVId
40
41 -- * Create Macaroons
42 , create
43 , inspect
44 , addFirstPartyCaveat
45 , addThirdPartyCaveat
46
47 -- * Prepare Macaroons for transfer
48 , serialize
49 ) where
50
51import Crypto.Cipher.AES
52import Crypto.Hash
53import Data.Byteable
54import qualified Data.ByteString as BS
55import qualified Data.ByteString.Base64.URL as B64
56import qualified Data.ByteString.Char8 as B8
57import Data.Hex
58
59import Crypto.Macaroon.Internal
60
61-- | Create a Macaroon from its key, identifier and location
62create :: Key -> Key -> Location -> Macaroon
63create secret ident loc = MkMacaroon loc ident [] (toBytes (hmac derivedKey ident :: HMAC SHA256))
64 where
65 derivedKey = toBytes $ (hmac "macaroons-key-generator" secret :: HMAC SHA256)
66
67caveatLoc :: Caveat -> Location
68caveatLoc = cl
69
70caveatId :: Caveat -> Key
71caveatId = cid
72
73caveatVId :: Caveat -> Key
74caveatVId = vid
75
76inspect :: Macaroon -> String
77inspect m = unlines [ "location " ++ show (location m)
78 , "identifier " ++ show (identifier m)
79 , (concatMap (showCav (location m)) (caveats m))
80 , "signature " ++ show (hex $ signature m)
81 ]
82 where
83 showCav loc c | cl c == loc && vid c == BS.empty = "cid " ++ show (cid c)
84 | otherwise = unlines [ "cid " ++ show (cid c)
85 , "vid " ++ show (vid c)
86 , "cl " ++ show (cl c)
87 ]
88
89serialize :: Macaroon -> BS.ByteString
90serialize m = B8.filter (/= '=') . B64.encode $ packets
91 where
92 packets = BS.concat [ putPacket "location" (location m)
93 , putPacket "identifier" (identifier m)
94 , caveatPackets
95 , putPacket "signature" (signature m)
96 ]
97 caveatPackets = BS.concat $ map (cavPacket (location m)) (caveats m)
98 cavPacket loc c | cl c == loc && vid c == BS.empty = putPacket "cid" (cid c)
99 | otherwise = BS.concat [ putPacket "cid" (cid c)
100 , putPacket "vid" (vid c)
101 , putPacket "cl" (cl c)
102 ]
103
104
105
106-- | Add a first party Caveat to a Macaroon, with its identifier
107addFirstPartyCaveat :: Key -> Macaroon -> Macaroon
108addFirstPartyCaveat ident m = addCaveat (location m) ident BS.empty m
109
110-- |Add a third party Caveat to a Macaroon, using its location, identifier and
111-- verification key
112addThirdPartyCaveat :: Key
113 -> Key
114 -> Location
115 -> Macaroon
116 -> Macaroon
117addThirdPartyCaveat key cid loc m = addCaveat loc cid vid m
118 where
119 vid = encryptECB (initAES (signature m)) key
120
121
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 @@
1{-|
2Module : Crypto.Macaroon.Binder
3Copyright : (c) 2015 Julien Tanguy
4License : BSD3
5
6Maintainer : julien.tanguy@jhome.fr
7Stability : experimental
8Portability : portable
9
10
11
12-}
13module Crypto.Macaroon.Binder where
14
15import Crypto.Hash
16import Data.Byteable
17import qualified Data.ByteString as BS
18
19import Crypto.Macaroon.Internal
20
21-- | Datatype for binding discharging and authorizing macaroons together
22newtype Binder = Binder { bind :: Macaroon -> Macaroon -> BS.ByteString }
23
24
25-- | Binder which concatenates the two signatures and hashes them
26hashSigs :: Binder
27hashSigs = Binder $ \m m' -> toBytes $ (HMAC . hash $ BS.append (toBytes $ signature m') (toBytes $ signature m) :: HMAC SHA256)
28
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 @@
1{-# LANGUAGE OverloadedStrings #-}
2{-|
3Module : Crypto.Macaroon.Internal
4Copyright : (c) 2015 Julien Tanguy
5License : BSD3
6
7Maintainer : julien.tanguy@jhome.fr
8Stability : experimental
9Portability : portable
10
11
12Internal representation of a macaroon
13-}
14module Crypto.Macaroon.Internal where
15
16
17import Control.DeepSeq
18import Crypto.Cipher.AES
19import Crypto.Hash
20import Data.Byteable
21import qualified Data.ByteString as BS
22import qualified Data.ByteString.Base64 as B64
23import qualified Data.ByteString.Char8 as B8
24import Data.Char
25import Data.Hex
26import Data.Serialize
27import Data.Word
28
29-- |Type alias for Macaroons and Caveat keys and identifiers
30type Key = BS.ByteString
31
32-- |Type alias For Macaroons and Caveat locations
33type Location = BS.ByteString
34
35type Sig = BS.ByteString
36
37-- | Main structure of a macaroon
38data Macaroon = MkMacaroon { location :: Location
39 -- ^ Target location
40 , identifier :: Key
41 -- ^ Macaroon Identifier
42 , caveats :: [Caveat]
43 -- ^ List of caveats
44 , signature :: Sig
45 -- ^ Macaroon HMAC signature
46 } deriving (Eq)
47
48
49instance NFData Macaroon where
50 rnf (MkMacaroon loc ident cavs sig) = rnf loc `seq` rnf ident `seq` rnf cavs `seq` rnf sig
51
52
53-- | Caveat structure
54data Caveat = MkCaveat { cid :: Key
55 -- ^ Caveat identifier
56 , vid :: Key
57 -- ^ Caveat verification key identifier
58 , cl :: Location
59 -- ^ Caveat target location
60
61 } deriving (Eq)
62
63instance NFData Caveat where
64 rnf (MkCaveat cid vid cl) = rnf cid `seq` rnf vid `seq` rnf cl
65
66
67putPacket :: BS.ByteString -> BS.ByteString -> BS.ByteString
68putPacket key dat = BS.concat [
69 B8.map toLower . hex . encode $ (fromIntegral size :: Word16)
70 , key
71 , " "
72 , dat
73 , "\n"
74 ]
75 where
76 size = 4 + 2 + BS.length key + BS.length dat
77
78addCaveat :: Location
79 -> Key
80 -> Key
81 -> Macaroon
82 -> Macaroon
83addCaveat loc cid vid m = m { caveats = cavs ++ [cav'], signature = sig}
84 where
85 cavs = caveats m
86 cav' = MkCaveat cid vid loc
87 sig = toBytes $ (hmac (signature m) (BS.append vid cid) :: HMAC SHA256)
88
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 @@
1{-# LANGUAGE OverloadedStrings #-}
2{-|
3Copyright : (c) 2015 Julien Tanguy
4License : BSD3
5
6Maintainer : julien.tanguy@jhome.fr
7
8
9This test suite is based on the pymacaroons test suite:
10<https://github.com/ecordell/pymacaroons>
11-}
12module Crypto.Macaroon.Tests where
13
14import Data.Byteable
15import qualified Data.ByteString.Char8 as B8
16import Data.Hex
17import Test.Tasty
18import Test.Tasty.HUnit
19
20import Crypto.Macaroon
21
22tests :: TestTree
23tests = testGroup "Crypto.Macaroon" [ basicSignature
24 , basicSerialize
25 , basicMint
26 , basicMintTrimmed
27 ]
28
29
30m :: Macaroon
31m = create secret key loc
32 where
33 secret = B8.pack "this is our super secret key; only we should know it"
34 key = B8.pack "we used our secret key"
35 loc = B8.pack "http://mybank/"
36
37m2 :: Macaroon
38m2 = addFirstPartyCaveat "test = caveat" m
39
40m3 :: Macaroon
41m3 = addFirstPartyCaveat "test = acaveat" m
42
43m4 :: Macaroon
44m4 = addThirdPartyCaveat caveat_key caveat_id caveat_loc n
45 where
46 n = addFirstPartyCaveat "account = 3735928559" $ create sec key loc
47 key = B8.pack "we used our other secret key"
48 loc = B8.pack "http://mybank/"
49 sec = B8.pack "this is a different super-secret key; never use the same secret twice"
50 caveat_key = B8.pack "4; guaranteed random by a fair toss of the dice"
51 caveat_id = B8.pack "this was how we remind auth of key/pred"
52 caveat_loc = B8.pack "http://auth.mybank/"
53
54
55basicSignature = testCase "Basic signature" $
56 "E3D9E02908526C4C0039AE15114115D97FDD68BF2BA379B342AAF0F617D0552F" @=? (hex . signature) m
57
58basicSerialize = testCase "Serialization" $
59 "MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudG\
60 \lmaWVyIHdlIHVzZWQgb3VyIHNlY3JldCBrZXkKMDAyZnNpZ25h\
61 \dHVyZSDj2eApCFJsTAA5rhURQRXZf91ovyujebNCqvD2F9BVLwo" @=? serialize m
62
63basicMint = testCase "First Party Caveat" $
64 "MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudGlmaWVyIHdlIHVzZ\
65 \WQgb3VyIHNlY3JldCBrZXkKMDAxNmNpZCB0ZXN0ID0gY2F2ZWF0CjAwMmZzaWduYXR1cmUgGXusegR\
66 \K8zMyhluSZuJtSTvdZopmDkTYjOGpmMI9vWcK" @=? serialize m2
67
68basicMintTrimmed = testCase "Trimmed base64" $
69 "MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudGlmaWVyIHdlIHVz\
70 \ZWQgb3VyIHNlY3JldCBrZXkKMDAxN2NpZCB0ZXN0ID0gYWNhdmVhdAowMDJmc2ln\
71 \bmF0dXJlIJRJ_V3WNJQnqlVq5eez7spnltwU_AXs8NIRY739sHooCg" @=? serialize m3
72
73basicThirdParty = testCase "Third Party Caveat" $
74 "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 @@
1{-#LANGUAGE OverloadedStrings#-}
2
3import Crypto.Hash
4import Data.ByteString (ByteString)
5import qualified Data.ByteString as B
6import Data.Hex
7import Data.Byteable
8
9import Test.Tasty
10import Test.Tasty.HUnit
11
12import qualified Crypto.Macaroon.Tests
13
14main = defaultMain tests
15
16tests :: TestTree
17tests = testGroup "Tests" [ sanityCheck
18 , Crypto.Macaroon.Tests.tests
19 ]
20
21sanityCheck :: TestTree
22sanityCheck = testGroup "Python HMAC Sanity check" [ checkKey
23 , checkMac1
24 , checkMac2
25 , checkMac3
26 , checkMac4
27 ]
28
29
30secret :: ByteString
31secret = "this is our super secret key; only we should know it"
32
33public :: ByteString
34public = "we used our secret key"
35
36key :: ByteString
37key = B.take 32 secret
38
39mac1 :: ByteString
40mac1 = toBytes $ (hmac key public :: HMAC SHA256)
41
42mac2 :: ByteString
43mac2 = toBytes $ (hmac mac1 "account = 3735928559" :: HMAC SHA256)
44
45mac3 :: ByteString
46mac3 = toBytes $ (hmac mac2 "time < 2015-01-01T00:00" :: HMAC SHA256)
47
48mac4 :: ByteString
49mac4 = toBytes $ (hmac mac3 "email = alice@example.org" :: HMAC SHA256)
50
51
52checkKey = testCase "Truncated key" $
53 key @?= "this is our super secret key; on"
54
55checkMac1 = testCase "HMAC key" $
56 "C60B4B3540BB1B2F2EF28D1C895691CC4A5E07A38A9D3B1C3379FB485293372F" @=? hex mac1
57
58checkMac2 = testCase "HMAC key account" $
59 "5C933DC9A7D036DFCD1740B4F26D737397A1FF635EAC900F3226973503CAAAA5" @=? hex mac2
60
61checkMac3 = testCase "HMAC key account time" $
62 "7A559B20C8B607009EBCE138C200585E9D0DECA6D23B3EAD6C5E0BA6861D3858" @=? hex mac3
63
64checkMac4 = testCase "HMAC key account time email" $
65 "E42BBB02A9A5A303483CB6295C497AE51AD1D5CB10003CBE548D907E7E62F5E4" @=? hex mac4
66