├── .gitignore ├── rebar.config ├── rebar.lock ├── src ├── macaroons.app.src ├── macaroon.hrl ├── macaroon_utils.erl ├── macaroon_verifier.erl └── macaroon.erl ├── LICENSE.md ├── test └── libmacaroons_example_tests.erl └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | _build/ 3 | .idea/ 4 | doc 5 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [ 2 | {enacl, ".*", {git, "https://github.com/jlouis/enacl.git", {tag, "0.16.0"}}}, 3 | {base64url, ".*", {git, "https://github.com/dvv/base64url.git", {tag, "v1.0"}}} 4 | ]}. 5 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | [{<<"base64url">>, 2 | {git,"https://github.com/dvv/base64url.git", 3 | {ref,"f2c64ed8b9bebc536fad37ad97243452b674b837"}}, 4 | 0}, 5 | {<<"enacl">>, 6 | {git,"https://github.com/jlouis/enacl.git", 7 | {ref,"61be95caadaaceae9f8d0cad7f5149ce3f44b65f"}}, 8 | 0}]. 9 | -------------------------------------------------------------------------------- /src/macaroons.app.src: -------------------------------------------------------------------------------- 1 | {application, macaroons, 2 | [ 3 | {description, "An Erlang Macaroons library compatible with libmacaroons"}, 4 | {vsn, git}, 5 | {maintainers, ["Konrad Zemek"]}, 6 | {registered, []}, 7 | {applications, [ 8 | kernel, 9 | stdlib 10 | ]}, 11 | {env, []}, 12 | {licenses, ["BSD 3-Clause"]}, 13 | {links, [{"GitHub", "https://github.com/kzemek/macaroons"}]}, 14 | {build_tools, [<<"rebar">>, <<"rebar3">>]} 15 | ]}. 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016, Konrad Zemek 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /src/macaroon.hrl: -------------------------------------------------------------------------------- 1 | %%%----------------------------------------------------------------------------- 2 | %%% @author Konrad Zemek 3 | %%% @copyright (C) 2015, Konrad Zemek 4 | %%% All rights reserved. 5 | %%% Redistribution and use in source and binary forms, with or without 6 | %%% modification, are permitted provided that the following conditions are met: 7 | %%% 8 | %%% 1. Redistributions of source code must retain the above copyright notice, 9 | %%% this list of conditions and the following disclaimer. 10 | %%% 11 | %%% 2. Redistributions in binary form must reproduce the above copyright notice, 12 | %%% this list of conditions and the following disclaimer in the documentation 13 | %%% and/or other materials provided with the distribution. 14 | %%% 15 | %%% 3. Neither the name of the copyright holder nor the names of its 16 | %%% contributors may be used to endorse or promote products derived from this 17 | %%% software without specific prior written permission. 18 | %%% 19 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | %%% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | %%% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | %%% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | %%% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | %%% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | %%% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | %%% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | %%% POSSIBILITY OF SUCH DAMAGE. 30 | %%% @end 31 | %%%----------------------------------------------------------------------------- 32 | %%% @doc 33 | %%% Common definitions for macaroon modules. 34 | %%% @end 35 | %%%----------------------------------------------------------------------------- 36 | 37 | %% Constants necessary for compatibility with libmacaroons. 38 | -define(HMAC_HASH_ALGORITHM, sha256). 39 | -define(HMAC_KEYBYTES, 32). 40 | -define(LIBMACAROONS_MAGIC_KEY, <<"macaroons-key-generator">>). 41 | -define(PACKET_HEADER_SIZE, 4). 42 | -define(LOCATION_KEY, <<"location">>). 43 | -define(IDENTIFIER_KEY, <<"identifier">>). 44 | -define(SIGNATURE_KEY, <<"signature">>). 45 | -define(CID_KEY, <<"cid">>). 46 | -define(VID_KEY, <<"vid">>). 47 | -define(CL_KEY, <<"cl">>). 48 | 49 | -record(macaroon, { 50 | identifier :: binary(), 51 | location :: binary(), 52 | caveats = [] :: [binary() | {binary(), binary(), binary()}], 53 | signature :: binary() 54 | }). 55 | -------------------------------------------------------------------------------- /src/macaroon_utils.erl: -------------------------------------------------------------------------------- 1 | %%%----------------------------------------------------------------------------- 2 | %%% @author Konrad Zemek 3 | %%% @copyright (C) 2015, Konrad Zemek 4 | %%% All rights reserved. 5 | %%% Redistribution and use in source and binary forms, with or without 6 | %%% modification, are permitted provided that the following conditions are met: 7 | %%% 8 | %%% 1. Redistributions of source code must retain the above copyright notice, 9 | %%% this list of conditions and the following disclaimer. 10 | %%% 11 | %%% 2. Redistributions in binary form must reproduce the above copyright notice, 12 | %%% this list of conditions and the following disclaimer in the documentation 13 | %%% and/or other materials provided with the distribution. 14 | %%% 15 | %%% 3. Neither the name of the copyright holder nor the names of its 16 | %%% contributors may be used to endorse or promote products derived from this 17 | %%% software without specific prior written permission. 18 | %%% 19 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | %%% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | %%% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | %%% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | %%% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | %%% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | %%% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | %%% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | %%% POSSIBILITY OF SUCH DAMAGE. 30 | %%% @end 31 | %%%----------------------------------------------------------------------------- 32 | %%% @doc 33 | %%% Common functions for macaroon and macaroon_verifier modules. 34 | %%% @end 35 | %%%----------------------------------------------------------------------------- 36 | -module(macaroon_utils). 37 | -author("Konrad Zemek"). 38 | 39 | -include("macaroon.hrl"). 40 | 41 | %% API 42 | -export([derive_key/1, bind_signature/2, macaroon_hash2/3, hex_encode/1]). 43 | 44 | %%%============================================================================= 45 | %%% API 46 | %%%============================================================================= 47 | 48 | -spec derive_key(VariableKey :: iodata()) -> binary(). 49 | derive_key(VariableKey) -> 50 | KeySize = ?HMAC_KEYBYTES, 51 | ZeroesNum = KeySize - byte_size(?LIBMACAROONS_MAGIC_KEY), 52 | Key = <>, 54 | crypto:hmac(?HMAC_HASH_ALGORITHM, Key, VariableKey). 55 | 56 | 57 | -spec bind_signature(ParentSig :: binary(), DischargeSig :: binary()) -> 58 | binary(). 59 | bind_signature(ParentSig, ParentSig) -> ParentSig; 60 | bind_signature(ParentSig, DischargeSig) -> 61 | KeySize = ?HMAC_KEYBYTES, 62 | Key = <<0:KeySize/little-signed-integer-unit:8>>, 63 | macaroon_utils:macaroon_hash2(ParentSig, DischargeSig, Key). 64 | 65 | 66 | -spec macaroon_hash2(Data1 :: iodata(), Data2 :: iodata(), Key :: iodata()) -> 67 | binary(). 68 | macaroon_hash2(Data1, Data2, Key) -> 69 | Hash1 = crypto:hmac(?HMAC_HASH_ALGORITHM, Key, Data1), 70 | Hash2 = crypto:hmac(?HMAC_HASH_ALGORITHM, Key, Data2), 71 | crypto:hmac(?HMAC_HASH_ALGORITHM, Key, [Hash1, Hash2]). 72 | 73 | 74 | -spec hex_encode(Data :: binary()) -> binary(). 75 | hex_encode(Data) -> 76 | <<<<(string:to_lower(Y))>> || 77 | <> <= Data, Y <- integer_to_list(X, 16)>>. 78 | -------------------------------------------------------------------------------- /test/libmacaroons_example_tests.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Konrad Zemek 3 | %%% @copyright (C) 2015, Konrad Zemek 4 | %%% All rights reserved. 5 | %%% Redistribution and use in source and binary forms, with or without 6 | %%% modification, are permitted provided that the following conditions are met: 7 | %%% 8 | %%% 1. Redistributions of source code must retain the above copyright notice, 9 | %%% this list of conditions and the following disclaimer. 10 | %%% 11 | %%% 2. Redistributions in binary form must reproduce the above copyright notice, 12 | %%% this list of conditions and the following disclaimer in the documentation 13 | %%% and/or other materials provided with the distribution. 14 | %%% 15 | %%% 3. Neither the name of the copyright holder nor the names of its 16 | %%% contributors may be used to endorse or promote products derived from this 17 | %%% software without specific prior written permission. 18 | %%% 19 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | %%% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | %%% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | %%% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | %%% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | %%% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | %%% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | %%% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | %%% POSSIBILITY OF SUCH DAMAGE. 30 | %%% @end 31 | %%%-------------------------------------------------------------------- 32 | %%% @doc 33 | %%% Tests for Macaroons checking if libmacaroons examples are correctly handled. 34 | %%% @end 35 | %%%------------------------------------------------------------------- 36 | -module(libmacaroons_example_tests). 37 | -author("Konrad Zemek"). 38 | 39 | -include_lib("eunit/include/eunit.hrl"). 40 | 41 | %%%=================================================================== 42 | %%% Test cases 43 | %%%=================================================================== 44 | 45 | libmacaroons_first_party_example_test() -> 46 | Secret = <<"this is our super secret key; only we should know it">>, 47 | Public = <<"we used our secret key">>, 48 | Location = <<"http://mybank/">>, 49 | 50 | M = macaroon:create(Location, Secret, Public), 51 | 52 | ?assertEqual(Public, macaroon:identifier(M)), 53 | ?assertEqual(Location, macaroon:location(M)), 54 | ?assertEqual( 55 | <<"e3d9e02908526c4c0039ae15114115d97fdd68bf2ba379b342aaf0f617d0552f">>, 56 | macaroon:signature(M)), 57 | ?assertEqual( 58 | {ok, <<"MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudGlmaWVyIHdlIHVzZWQgb3VyIHNlY3JldCBrZXkKMDAyZnNpZ25hdHVyZSDj2eApCFJsTAA5rhURQRXZf91ovyujebNCqvD2F9BVLwo">>}, 59 | macaroon:serialize(M)), 60 | 61 | M1 = macaroon:add_first_party_caveat(M, <<"account = 3735928559">>), 62 | ?assertEqual( 63 | <<"1efe4763f290dbce0c1d08477367e11f4eee456a64933cf662d79772dbb82128">>, 64 | macaroon:signature(M1)), 65 | 66 | M2 = macaroon:add_first_party_caveat(M1, <<"time < 2020-01-01T00:00">>), 67 | ?assertEqual( 68 | <<"b5f06c8c8ef92f6c82c6ff282cd1f8bd1849301d09a2db634ba182536a611c49">>, 69 | macaroon:signature(M2)), 70 | 71 | M3 = macaroon:add_first_party_caveat(M2, <<"email = alice@example.org">>), 72 | ?assertEqual( 73 | <<"ddf553e46083e55b8d71ab822be3d8fcf21d6bf19c40d617bb9fb438934474b6">>, 74 | macaroon:signature(M3)), 75 | 76 | {ok, Msg} = macaroon:serialize(M3), 77 | ?assertEqual({ok, M3}, macaroon:deserialize(Msg)), 78 | 79 | V = macaroon_verifier:create(), 80 | ?assertEqual( 81 | {error, {unverified_caveat, <<"account = 3735928559">>}}, 82 | macaroon_verifier:verify(V, M3, Secret)), 83 | 84 | V1 = macaroon_verifier:satisfy_exact(V, <<"account = 3735928559">>), 85 | V2 = macaroon_verifier:satisfy_exact(V1, <<"email = alice@example.org">>), 86 | V3 = macaroon_verifier:satisfy_exact(V2, <<"IP = 127.0.0.1">>), 87 | V4 = macaroon_verifier:satisfy_exact(V3, <<"browser = Chrome">>), 88 | V5 = macaroon_verifier:satisfy_exact(V4, <<"action = deposit">>), 89 | 90 | CheckTime = fun 91 | (<<"time < ", DateTime/binary>>) -> DateTime =:= <<"2020-01-01T00:00">>; 92 | (_) -> false 93 | end, 94 | 95 | V6 = macaroon_verifier:satisfy_general(V5, CheckTime), 96 | ?assertEqual(ok, macaroon_verifier:verify(V6, M3, Secret)), 97 | 98 | N = macaroon:add_first_party_caveat(M3, <<"action = deposit">>), 99 | ?assertEqual(ok, macaroon_verifier:verify(V6, N, Secret)), 100 | 101 | N2 = macaroon:add_first_party_caveat(M, <<"OS = Windows XP">>), 102 | ?assertEqual( 103 | {error, {unverified_caveat, <<"OS = Windows XP">>}}, 104 | macaroon_verifier:verify(V6, N2, Secret)), 105 | 106 | N3 = macaroon:add_first_party_caveat(M, <<"time < 2014-01-01T00:00">>), 107 | ?assertEqual( 108 | {error, {unverified_caveat, <<"time < 2014-01-01T00:00">>}}, 109 | macaroon_verifier:verify(V6, N3, Secret)), 110 | 111 | ?assertEqual( 112 | {error, {bad_signature_for_macaroon, Public}}, 113 | macaroon_verifier:verify(V6, M, 114 | <<"this is not the secret we were looking for">>)), 115 | 116 | {ok, N4} = macaroon:deserialize( 117 | <<"MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudGlmaWVyIHdlIHVzZWQgb3VyIHNl\nY3JldCBrZXkKMDAxZGNpZCBhY2NvdW50ID0gMzczNTkyODU1OQowMDIwY2lkIHRpbWUgPCAyMDIw\nLTAxLTAxVDAwOjAwCjAwMjJjaWQgZW1haWwgPSBhbGljZUBleGFtcGxlLm9yZwowMDJmc2lnbmF0\ndXJlID8f19FL+bkC9p/aoMmIecC7GxdOcLVyUnrv6lJMM7NSCg==\n">>), 118 | ?assertNotEqual(macaroon:signature(M3), macaroon:signature(N4)), 119 | ?assertEqual( 120 | {error, {bad_signature_for_macaroon, Public}}, 121 | macaroon_verifier:verify(V6, N4, Secret)). 122 | 123 | 124 | libmacaroons_third_party_example_test() -> 125 | Secret = 126 | <<"this is a different super-secret key; never use the same secret twice">>, 127 | Public = <<"we used our other secret key">>, 128 | Location = <<"http://mybank/">>, 129 | M = macaroon:create(Location, Secret, Public), 130 | M1 = macaroon:add_first_party_caveat(M, <<"account = 3735928559">>), 131 | 132 | CaveatKey = <<"4; guaranteed random by a fair toss of the dice">>, 133 | Identifier = <<"this was how we remind auth of key/pred">>, 134 | M2 = macaroon:add_third_party_caveat(M1, <<"http://auth.mybank/">>, 135 | CaveatKey, Identifier), 136 | 137 | ?assertEqual( 138 | [{<<"http://auth.mybank/">>, <<"this was how we remind auth of key/pred">>}], 139 | macaroon:third_party_caveats(M2)), 140 | 141 | D = macaroon:create("http://auth.mybank/", CaveatKey, Identifier), 142 | D2 = macaroon:add_first_party_caveat(D, "time < 2020-01-01T00:00"), 143 | DP = macaroon:prepare_for_request(M2, D2), 144 | 145 | ?assertNotEqual(macaroon:signature(D2), macaroon:signature(DP)), 146 | 147 | V = macaroon_verifier:create(), 148 | V1 = macaroon_verifier:satisfy_exact(V, <<"account = 3735928559">>), 149 | 150 | CheckTime = fun 151 | (<<"time < ", DateTime/binary>>) -> DateTime =:= <<"2020-01-01T00:00">>; 152 | (_) -> false 153 | end, 154 | 155 | V2 = macaroon_verifier:satisfy_general(V1, CheckTime), 156 | 157 | ?assertEqual(ok, macaroon_verifier:verify(V2, M2, Secret, [DP])), 158 | ?assertEqual( 159 | {error, {bad_signature_for_macaroon, Identifier}}, 160 | macaroon_verifier:verify(V2, M2, Secret, [D2])). 161 | -------------------------------------------------------------------------------- /src/macaroon_verifier.erl: -------------------------------------------------------------------------------- 1 | %%%----------------------------------------------------------------------------- 2 | %%% @author Konrad Zemek 3 | %%% @copyright (C) 2015, Konrad Zemek 4 | %%% All rights reserved. 5 | %%% Redistribution and use in source and binary forms, with or without 6 | %%% modification, are permitted provided that the following conditions are met: 7 | %%% 8 | %%% 1. Redistributions of source code must retain the above copyright notice, 9 | %%% this list of conditions and the following disclaimer. 10 | %%% 11 | %%% 2. Redistributions in binary form must reproduce the above copyright notice, 12 | %%% this list of conditions and the following disclaimer in the documentation 13 | %%% and/or other materials provided with the distribution. 14 | %%% 15 | %%% 3. Neither the name of the copyright holder nor the names of its 16 | %%% contributors may be used to endorse or promote products derived from this 17 | %%% software without specific prior written permission. 18 | %%% 19 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | %%% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | %%% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | %%% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | %%% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | %%% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | %%% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | %%% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | %%% POSSIBILITY OF SUCH DAMAGE. 30 | %%% @end 31 | %%%----------------------------------------------------------------------------- 32 | %%% @doc 33 | %%% This module contains operations for verifying macaroons. 34 | %%% @end 35 | %%%----------------------------------------------------------------------------- 36 | -module(macaroon_verifier). 37 | -author("Konrad Zemek"). 38 | 39 | -include("macaroon.hrl"). 40 | 41 | %% API 42 | -export([create/0, satisfy_exact/2, satisfy_general/2, verify/3, verify/4]). 43 | 44 | %% Types 45 | -type predicate() :: fun((binary()) -> boolean()). 46 | -type auth_error() :: 47 | {unverified_caveat, Caveat :: binary()} 48 | | {bad_signature_for_macaroon, MacaroonId :: binary()} 49 | | {failed_to_decrypt_caveat, CaveatId :: binary()} 50 | | {no_discharge_macaroon_for_caveat, CaveatId :: binary()}. 51 | 52 | -record(verifier, { 53 | exact = sets:new() :: sets:set(binary()), 54 | general = [] :: [predicate()] 55 | }). 56 | 57 | -opaque verifier() :: #verifier{}. 58 | -export_type([predicate/0, verifier/0, auth_error/0]). 59 | 60 | %%%=================================================================== 61 | %%% API 62 | %%%=================================================================== 63 | 64 | %%------------------------------------------------------------------------------ 65 | %% @doc 66 | %% Creates a new verifier. 67 | %% @end 68 | %%------------------------------------------------------------------------------ 69 | -spec create() -> verifier(). 70 | create() -> 71 | #verifier{}. 72 | 73 | %%------------------------------------------------------------------------------ 74 | %% @doc 75 | %% Returns a new verifier that additionally accepts given exact caveat. 76 | %% @end 77 | %%------------------------------------------------------------------------------ 78 | -spec satisfy_exact(Verifier :: verifier(), Predicate :: iodata()) -> 79 | verifier(). 80 | satisfy_exact(#verifier{} = V, Predicate) when not is_binary(Predicate) -> 81 | satisfy_exact(V, iolist_to_binary(Predicate)); 82 | satisfy_exact(#verifier{} = V, Predicate) -> 83 | V#verifier{exact = sets:add_element(Predicate, V#verifier.exact)}. 84 | 85 | %%------------------------------------------------------------------------------ 86 | %% @doc 87 | %% Returns a new verifier that uses an additional predicate function to verify 88 | %% caveats. 89 | %% @end 90 | %%------------------------------------------------------------------------------ 91 | -spec satisfy_general(Verifier :: verifier(), Predicate :: predicate()) -> 92 | verifier(). 93 | satisfy_general(#verifier{} = V, Predicate) when is_function(Predicate, 1) -> 94 | V#verifier{general = [Predicate | V#verifier.general]}. 95 | 96 | %%------------------------------------------------------------------------------ 97 | %% @equiv 98 | %% verify(Verifier, Macaroon, Key, []) 99 | %% @end 100 | %%------------------------------------------------------------------------------ 101 | -spec verify(Verifier :: verifier(), Macaroon :: macaroon:macaroon(), 102 | Key :: iodata()) -> 103 | ok | {error, auth_error()}. 104 | verify(Verifier, Macaroon, Key) -> 105 | verify(Verifier, Macaroon, Key, []). 106 | 107 | %%------------------------------------------------------------------------------ 108 | %% @doc 109 | %% Verifies a macaroon using given discharge macaroons and preconfigured 110 | %% verifier. 111 | %% @end 112 | %%------------------------------------------------------------------------------ 113 | -spec verify(Verifier :: verifier(), Macaroon :: macaroon:macaroon(), 114 | Key :: iodata(), DischargeMacaroons :: [macaroon:macaroon()]) -> 115 | ok | {error, auth_error()}. 116 | verify(#verifier{} = V, #macaroon{} = M, Key, DischargeMacaroons) -> 117 | DerivedKey = macaroon_utils:derive_key(Key), 118 | verify(M#macaroon.signature, V, M, DerivedKey, DischargeMacaroons). 119 | 120 | 121 | %%%=================================================================== 122 | %%% Internal functions 123 | %%%=================================================================== 124 | 125 | -spec verify(ParentSig :: binary(), Verifier :: verifier(), 126 | Macaroon :: macaroon:macaroon(), Key :: binary(), 127 | DischargeMacaroons :: [macaroon:macaroon()]) -> 128 | ok | {error, auth_error()}. 129 | verify(ParentSig, V, M, Key, DischargeMacaroons) -> 130 | BaseSignature = 131 | crypto:hmac(?HMAC_HASH_ALGORITHM, Key, M#macaroon.identifier), 132 | 133 | VerifyResult = 134 | verify_loop(ParentSig, V, DischargeMacaroons, 135 | lists:reverse(M#macaroon.caveats), BaseSignature), 136 | 137 | case VerifyResult of 138 | {ok, Signature} -> 139 | BoundSignature = 140 | macaroon_utils:bind_signature(ParentSig, Signature), 141 | 142 | case BoundSignature =:= M#macaroon.signature of 143 | true -> ok; 144 | false -> 145 | {error, {bad_signature_for_macaroon, M#macaroon.identifier}} 146 | end; 147 | 148 | {error, Reason} -> {error, Reason} 149 | end. 150 | 151 | 152 | -spec verify_loop(ParentSig :: binary(), Verifier :: verifier(), 153 | DMs :: [macaroon:macaroon()], 154 | Caveats :: [binary() | {binary(), binary(), binary()}], 155 | Signature :: binary()) -> 156 | {ok, FinalSig :: binary()} | {error, auth_error()}. 157 | verify_loop(_ParentSig, _V, _DMs, [], Signature) -> {ok, Signature}; 158 | 159 | verify_loop(ParentSig, V, DMs, [{Id, Vid, _Location} | Caveats], Signature) -> 160 | NonceSize = enacl:secretbox_nonce_size(), 161 | <> = Vid, 162 | 163 | OpenBoxResult = 164 | enacl:secretbox_open(CipherText, Nonce, Signature), 165 | 166 | case OpenBoxResult of 167 | {error, _} -> {error, {failed_to_decrypt_caveat, Id}}; 168 | {ok, Key} -> 169 | {DMsBefore, DMsAfterWithPivot} = 170 | lists:splitwith(fun(M) -> 171 | M#macaroon.identifier =/= Id end, DMs), 172 | 173 | case DMsAfterWithPivot of 174 | [] -> {error, {no_discharge_macaroon_for_caveat, Id}}; 175 | [DM | DMsAfter] -> 176 | OtherDMs = DMsBefore ++ DMsAfter, 177 | case verify(ParentSig, V, DM, Key, OtherDMs) of 178 | {error, Reason} -> {error, Reason}; 179 | ok -> 180 | NewSig = macaroon_utils:macaroon_hash2(Vid, Id, 181 | Signature), 182 | verify_loop(ParentSig, V, OtherDMs, Caveats, NewSig) 183 | end 184 | end 185 | end; 186 | 187 | verify_loop(ParentSig, V, DMs, [Caveat | Caveats], Signature) -> 188 | case caveat_verifies(V, Caveat) of 189 | false -> {error, {unverified_caveat, Caveat}}; 190 | true -> 191 | NewSig = crypto:hmac(?HMAC_HASH_ALGORITHM, Signature, Caveat), 192 | verify_loop(ParentSig, V, DMs, Caveats, NewSig) 193 | end. 194 | 195 | 196 | -spec caveat_verifies(Verifier :: verifier(), Caveat :: binary()) -> boolean(). 197 | caveat_verifies(V, Caveat) -> 198 | case sets:is_element(Caveat, V#verifier.exact) of 199 | true -> true; 200 | false -> 201 | lists:any(fun(General) -> General(Caveat) end, V#verifier.general) 202 | end. 203 | -------------------------------------------------------------------------------- /src/macaroon.erl: -------------------------------------------------------------------------------- 1 | %%%----------------------------------------------------------------------------- 2 | %%% @author Konrad Zemek 3 | %%% @copyright (C) 2015, Konrad Zemek 4 | %%% All rights reserved. 5 | %%% Redistribution and use in source and binary forms, with or without 6 | %%% modification, are permitted provided that the following conditions are met: 7 | %%% 8 | %%% 1. Redistributions of source code must retain the above copyright notice, 9 | %%% this list of conditions and the following disclaimer. 10 | %%% 11 | %%% 2. Redistributions in binary form must reproduce the above copyright notice, 12 | %%% this list of conditions and the following disclaimer in the documentation 13 | %%% and/or other materials provided with the distribution. 14 | %%% 15 | %%% 3. Neither the name of the copyright holder nor the names of its 16 | %%% contributors may be used to endorse or promote products derived from this 17 | %%% software without specific prior written permission. 18 | %%% 19 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | %%% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | %%% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | %%% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | %%% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | %%% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | %%% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | %%% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | %%% POSSIBILITY OF SUCH DAMAGE. 30 | %%% @end 31 | %%%----------------------------------------------------------------------------- 32 | %%% @doc 33 | %%% This module contains operations for manipulating and inspecting macaroons. 34 | %%% @end 35 | %%%----------------------------------------------------------------------------- 36 | -module(macaroon). 37 | -author("Konrad Zemek"). 38 | 39 | -include("macaroon.hrl"). 40 | 41 | %% API 42 | -export([create/3, add_first_party_caveat/2, add_third_party_caveat/4, 43 | prepare_for_request/2]). 44 | -export([is_macaroon/1, third_party_caveats/1, location/1, signature/1, 45 | identifier/1, inspect/1]). 46 | -export([serialize/1, deserialize/1]). 47 | -export([suggested_secret_length/0]). 48 | 49 | %% Types 50 | -type macaroon() :: #macaroon{}. 51 | -export_type([macaroon/0]). 52 | 53 | %%%============================================================================= 54 | %%% API 55 | %%%============================================================================= 56 | 57 | %%------------------------------------------------------------------------------ 58 | %% @doc 59 | %% Creates a macaroon with set location (hint), key (secret) and id (public) 60 | %% attributes. 61 | %% @end 62 | %%------------------------------------------------------------------------------ 63 | -spec create(Location :: iodata(), Key :: iodata(), Id :: iodata()) -> 64 | macaroon(). 65 | create(Location, Key, Id) when not is_binary(Location); not is_binary(Id) -> 66 | create(iolist_to_binary(Location), Key, iolist_to_binary(Id)); 67 | create(Location, Key, Id) -> 68 | DerivedKey = macaroon_utils:derive_key(Key), 69 | Signature = crypto:hmac(?HMAC_HASH_ALGORITHM, DerivedKey, Id), 70 | #macaroon{identifier = Id, location = Location, signature = Signature}. 71 | 72 | %%------------------------------------------------------------------------------ 73 | %% @doc 74 | %% Returns a new macaroon with new first-party caveat. 75 | %% @end 76 | %%------------------------------------------------------------------------------ 77 | -spec add_first_party_caveat(Macaroon :: macaroon(), Caveat :: iodata()) -> 78 | macaroon(). 79 | add_first_party_caveat(#macaroon{} = M, Caveat) when not is_binary(Caveat) -> 80 | add_first_party_caveat(M, iolist_to_binary(Caveat)); 81 | add_first_party_caveat(#macaroon{} = M, Caveat) -> 82 | Caveats = [Caveat | M#macaroon.caveats], 83 | NewSig = crypto:hmac(?HMAC_HASH_ALGORITHM, M#macaroon.signature, Caveat), 84 | M#macaroon{caveats = Caveats, signature = NewSig}. 85 | 86 | %%------------------------------------------------------------------------------ 87 | %% @doc 88 | %% Returns a new macaroon with new third-party caveat. 89 | %% The caveat is built from given location (hint), key (secret) and id (public) 90 | %% attributes. 91 | %% @end 92 | %%------------------------------------------------------------------------------ 93 | -spec add_third_party_caveat(Macaroon :: macaroon(), Location :: iodata(), 94 | Key :: iodata(), Id :: iodata()) -> 95 | macaroon(). 96 | add_third_party_caveat(#macaroon{} = M, Location, Key, Id) 97 | when not is_binary(Location); not is_binary(Id) -> 98 | add_third_party_caveat(M, iolist_to_binary(Location), 99 | Key, iolist_to_binary(Id)); 100 | add_third_party_caveat(#macaroon{} = M, Location, Key, Id) -> 101 | NonceSize = enacl:secretbox_nonce_size(), 102 | Nonce = enacl:randombytes(NonceSize), 103 | 104 | OldSig = M#macaroon.signature, 105 | DerivedKey = macaroon_utils:derive_key(Key), 106 | 107 | CipherText = enacl:secretbox(DerivedKey, Nonce, OldSig), 108 | 109 | Vid = <>, 110 | NewSig = macaroon_utils:macaroon_hash2(Vid, Id, OldSig), 111 | 112 | Caveat = {iolist_to_binary(Id), Vid, iolist_to_binary(Location)}, 113 | M#macaroon{caveats = [Caveat | M#macaroon.caveats], signature = NewSig}. 114 | 115 | %%------------------------------------------------------------------------------ 116 | %% @doc 117 | %% Returns a list of third-party caveats of the macaroon. 118 | %% Each caveat is represented as a {location (hint), id (public)} tuple. 119 | %% @end 120 | %%------------------------------------------------------------------------------ 121 | -spec third_party_caveats(Macaroon :: macaroon()) -> 122 | [{Location :: binary(), Id :: binary}]. 123 | third_party_caveats(#macaroon{caveats = Caveats}) -> 124 | lists:reverse(lists:filtermap(fun 125 | ({Id, _Vid, Location}) -> {true, {Location, Id}}; 126 | (_) -> false 127 | end, Caveats)). 128 | 129 | %%------------------------------------------------------------------------------ 130 | %% @doc 131 | %% Returns a new discharge macaroon bounded to the parent macaroon. 132 | %% @end 133 | %%------------------------------------------------------------------------------ 134 | -spec prepare_for_request(Macaroon :: macaroon(), Dispatch :: macaroon()) -> 135 | macaroon(). 136 | prepare_for_request(#macaroon{} = M, #macaroon{} = D) -> 137 | NewSig = macaroon_utils:bind_signature(M#macaroon.signature, 138 | D#macaroon.signature), 139 | D#macaroon{signature = NewSig}. 140 | 141 | %%------------------------------------------------------------------------------ 142 | %% @doc 143 | %% Returns if given term is a macaroon. 144 | %% @end 145 | %%------------------------------------------------------------------------------ 146 | -spec is_macaroon(term()) -> boolean(). 147 | is_macaroon(#macaroon{}) -> 148 | true; 149 | is_macaroon(_) -> 150 | false. 151 | 152 | %%------------------------------------------------------------------------------ 153 | %% @doc 154 | %% Returns macaroon's location (hint) attribute. 155 | %% @end 156 | %%------------------------------------------------------------------------------ 157 | -spec location(Macaroon :: macaroon()) -> binary(). 158 | location(#macaroon{} = M) -> 159 | M#macaroon.location. 160 | 161 | %%------------------------------------------------------------------------------ 162 | %% @doc 163 | %% Returns macaroon's signature. 164 | %% @end 165 | %%------------------------------------------------------------------------------ 166 | -spec signature(Macaroon :: macaroon()) -> binary(). 167 | signature(#macaroon{} = M) -> 168 | macaroon_utils:hex_encode(M#macaroon.signature). 169 | 170 | %%------------------------------------------------------------------------------ 171 | %% @doc 172 | %% Returns macaroon's identifier (public) attribute. 173 | %% @end 174 | %%------------------------------------------------------------------------------ 175 | -spec identifier(Macaroon :: macaroon()) -> binary(). 176 | identifier(#macaroon{} = M) -> 177 | M#macaroon.identifier. 178 | 179 | %%------------------------------------------------------------------------------ 180 | %% @doc 181 | %% Serializes the macaroon into base64url-encoded binary. 182 | %% The serialized format is compatible with libmacaroons reference 183 | %% implementation. 184 | %% @end 185 | %%------------------------------------------------------------------------------ 186 | -spec serialize(Macaroon :: macaroon()) -> 187 | {ok, binary()} | {error, {too_long, term()}}. 188 | serialize(#macaroon{} = M) -> 189 | try 190 | CaveatsData = 191 | lists:reverse(lists:map(fun 192 | ({Id, Vid, Location}) -> 193 | [ 194 | pack(?CID_KEY, Id), 195 | pack(?VID_KEY, Vid), 196 | pack(?CL_KEY, Location) 197 | ]; 198 | 199 | (Caveat) -> pack(?CID_KEY, Caveat) 200 | end, M#macaroon.caveats)), 201 | 202 | Data = [ 203 | pack(?LOCATION_KEY, M#macaroon.location), 204 | pack(?IDENTIFIER_KEY, M#macaroon.identifier), 205 | CaveatsData, 206 | pack(?SIGNATURE_KEY, M#macaroon.signature)], 207 | 208 | {ok, base64url:encode(iolist_to_binary(Data))} 209 | catch 210 | {cannot_serialize, Reason} -> {error, Reason} 211 | end. 212 | 213 | %%------------------------------------------------------------------------------ 214 | %% @doc 215 | %% Deserializes a macaroon from base64url-encoded binary. 216 | %% The serialized format must be compatible with libmacaroons reference 217 | %% implementation. 218 | %% @end 219 | %%------------------------------------------------------------------------------ 220 | -spec deserialize(Data :: iodata()) -> 221 | {ok, macaroon()} | {error, macaroon_invalid}. 222 | deserialize(Data) when not is_binary(Data) -> 223 | deserialize(iolist_to_binary(Data)); 224 | deserialize(Data) -> 225 | try 226 | DeserializedData = base64url:decode(Data), 227 | M = deserialize_lines(DeserializedData, #macaroon{}, undefined), 228 | {ok, M} 229 | catch 230 | _:_ -> {error, macaroon_invalid} 231 | end. 232 | 233 | %%------------------------------------------------------------------------------ 234 | %% @doc 235 | %% Returns a human-readable binary that describes the macaroon. 236 | %% The function is only intended for debugging, and can for example be used 237 | %% with ``io:format("~s", [inspect(M)])''. 238 | %% @end 239 | %%------------------------------------------------------------------------------ 240 | -spec inspect(Macaroon :: macaroon()) -> binary(). 241 | inspect(#macaroon{} = M) -> 242 | CaveatsData = 243 | lists:map(fun 244 | ({Id, Vid, Location}) -> 245 | HexVid = macaroon_utils:hex_encode(Vid), 246 | [ 247 | ?CID_KEY, <<" ">>, Id, <<"\n">>, 248 | ?VID_KEY, <<" ">>, HexVid, <<"\n">>, 249 | ?CL_KEY, <<" ">>, Location, <<"\n">> 250 | ]; 251 | (Caveat) -> [?CID_KEY, <<" ">>, Caveat, <<"\n">>] 252 | end, lists:reverse(M#macaroon.caveats)), 253 | 254 | HexSignature = macaroon_utils:hex_encode(M#macaroon.signature), 255 | 256 | iolist_to_binary([ 257 | ?LOCATION_KEY, <<" ">>, M#macaroon.location, <<"\n">>, 258 | ?IDENTIFIER_KEY, <<" ">>, M#macaroon.identifier, <<"\n">>, 259 | CaveatsData, 260 | ?SIGNATURE_KEY, <<" ">>, HexSignature, <<"\n">> 261 | ]). 262 | 263 | %%------------------------------------------------------------------------------ 264 | %% @doc 265 | %% Returns the ideal length of secret key used for creating macaroons. 266 | %% @end 267 | %%------------------------------------------------------------------------------ 268 | -spec suggested_secret_length() -> non_neg_integer(). 269 | suggested_secret_length() -> 270 | ?HMAC_KEYBYTES. 271 | 272 | %%%============================================================================= 273 | %%% Internal functions 274 | %%%============================================================================= 275 | 276 | -spec pack(Key :: binary(), Value :: binary()) -> iolist(). 277 | pack(Key, Value) -> 278 | DataSize = ?PACKET_HEADER_SIZE + 2 + byte_size(Key) + byte_size(Value), 279 | SizeEncoded = 280 | list_to_binary(string:to_lower(integer_to_list(DataSize, 16))), 281 | 282 | PaddingSize = ?PACKET_HEADER_SIZE - byte_size(SizeEncoded), 283 | case PaddingSize < 0 of 284 | true -> throw({cannot_serialize, {too_long, {Key, Value}}}); 285 | false -> 286 | [ 287 | binary:copy(<<"0">>, PaddingSize), SizeEncoded, 288 | Key, <<" ">>, Value, <<"\n">> 289 | ] 290 | end. 291 | 292 | -spec deserialize_lines(Data :: binary(), #macaroon{}, 293 | LastCidVid :: undefined | {binary(), binary()}) -> 294 | #macaroon{} | no_return(). 295 | deserialize_lines(<<>>, #macaroon{} = M, undefined) -> M; 296 | deserialize_lines(Data, #macaroon{} = M, LastCidVid) -> 297 | <> = Data, 298 | LineSize = binary_to_integer(BinLineSize, 16), 299 | ContentSize = LineSize - ?PACKET_HEADER_SIZE - 1, 300 | 301 | <> = DataWithoutHeader, 302 | [Key, Value] = binary:split(Content, <<" ">>), 303 | 304 | {NewM, NewLastCidVid} = 305 | case {Key, M, LastCidVid} of 306 | {?LOCATION_KEY, #macaroon{location = undefined}, _} -> 307 | {M#macaroon{location = Value}, LastCidVid}; 308 | 309 | {?IDENTIFIER_KEY, #macaroon{identifier = undefined}, _} -> 310 | {M#macaroon{identifier = Value}, LastCidVid}; 311 | 312 | {?SIGNATURE_KEY, #macaroon{signature = undefined}, _} -> 313 | {M#macaroon{signature = Value}, LastCidVid}; 314 | 315 | {?CID_KEY, #macaroon{caveats = Rest}, undefined} -> 316 | {M#macaroon{caveats = [Value | Rest]}, undefined}; 317 | 318 | {?VID_KEY, #macaroon{caveats = [Cid | Rest]}, undefined} -> 319 | {M#macaroon{caveats = Rest}, {Cid, Value}}; 320 | 321 | {?CL_KEY, #macaroon{caveats = Rest}, {Cid, Vid}} -> 322 | {M#macaroon{caveats = [{Cid, Vid, Value} | Rest]}, undefined} 323 | end, 324 | 325 | deserialize_lines(RestLines, NewM, NewLastCidVid). 326 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Note: This README file is a slightly adapted version of README from 2 | > [rescrv/libmacaroons]. 3 | 4 | Macaroons are Better Than Cookies! 5 | ================================== 6 | 7 | This library is [libmacaroons]-compatible Erlang implementation of [macaroons]. 8 | Macaroons are flexible authorization tokens that work great in distributed 9 | systems. Like cookies, macaroons are bearer tokens that enable applications to 10 | ascertain whether their holders' actions are authorized. But macaroons are 11 | better than cookies! 12 | 13 | Why Macaroons? 14 | -------------- 15 | 16 | Macaroons are great for authorization because they're similar enough to cookies 17 | to be immediately usable by developers, but they include several features not 18 | present in cookies or other token-based authorization schemes. In particular: 19 | 20 | - Delegation with Contextual Caveats (i.e., confinement of the usage context): 21 | Macaroons support delegation. Give your macaroon to another user, and they 22 | can act on your behalf, with the same authority. Cookies permit delegation 23 | as well, but the remaining features of macaroons make it much more safe and 24 | practical to pass around macaroons than cookies. In particular, macaroons 25 | can limit when, where, and by whom the delegated authority can be exercised 26 | (e.g., within one minute, from a machine that holds a certain key, or by a 27 | certain logged-in user), by using attenuation and third-party caveats. 28 | 29 | - Attenuation: Macaroons enable users to add caveats to the macaroon that 30 | attenuate how, when, and where it may be used. Unlike cookies, macaroons 31 | permit their holder to attenuate them before delegating. Whereas cookies and 32 | authorization tokens enable an application to get access to all of your data 33 | and to perform actions on your behalf with your full privileges, macaroons 34 | enable you to restrict what they can do. Those questionable startups that 35 | "just want the address book, we swear it," become a whole lot more secure 36 | when the target application supports macaroons, because macaroons enable you 37 | to add caveats that restrict what the application can do. 38 | 39 | - Proof-Carrying: Macaroons are efficient, because they carry their own proof 40 | of authorization---cryptographically secured, of course. A macaroon's 41 | caveats are constructed using chained HMAC functions, which makes it really 42 | easy to add a caveat, but impossible to remove a caveat. When you attenuate 43 | a macaroon and give it to another application, there is no way to strip the 44 | caveats from the macaroon. It's easy for the entity that created a macaroon 45 | to verify the embedded proof, but others cannot. 46 | 47 | - Third-Party Caveats: Macaroons allow caveats to specify predicates that are 48 | enforced by third parties. A macaroon with a third-party caveat will only be 49 | authorized when the third party certifies that the caveat is satisfied. This 50 | enables loosely coupled distributed systems to work together to authorize 51 | requests. For example, a data store can provide macaroons that are 52 | authorized if and only if the application's authentication service says that 53 | the user is authenticated. The user obtains a proof that it is 54 | authenticated from the authentication service, and presents this proof 55 | alongside the original macaroon to the storage service. The storage service 56 | can verify that the user is indeed authenticated, without knowing anything 57 | about the authentication service's implementation---in a standard 58 | implementation, the storage service can authorize the request without even 59 | communicating with the authentication service. 60 | 61 | - Simple Verification: Macaroons eliminate complexity in the authorization 62 | code of your application. Instead of hand-coding complex conditionals in 63 | each routine that deals with authorization, and hoping that this logic is 64 | globally consistent, you construct a general verifier for macaroons. This 65 | verifier knows how to soundly check the proofs embedded within macaroons to 66 | see if they do indeed authorize access. 67 | 68 | - Decoupled Authorization Logic: Macaroons separate the policy of your 69 | application (who can access what, when), from the mechanism (the code that 70 | actually upholds this policy). Because of the way the verifier is 71 | constructed, it is agnostic to the actual underlying policies it is 72 | enforcing. It simply observes the policy (in the form of an embedded proof) 73 | and certifies that the proof is correct. The policy itself is specified when 74 | macaroons are created, attenuated, and shared. You can easily audit this 75 | code within your application, and ensure that it is upheld everywhere. 76 | 77 | The rest of this document walks through the specifics of macaroons and see just 78 | how easy authorization can be. So pour a fresh cup of espresso to enjoy 79 | alongside your macaroons and read on! 80 | 81 | Installing Macaroons 82 | -------------------- 83 | 84 | Building `macaroons` has the following requirements: 85 | 86 | * [libsodium] installed on the system 87 | * C compiler 88 | * Erlang compiled with enabled dirty NIFs 89 | * [rebar] 90 | 91 | `macaroons` uses [enacl] to provide libsodium cryptographic functions in Erlang. 92 | 93 | Including `macaroons` is trivial, provided your Erlang application uses [rebar] 94 | for dependency management. Simply add the library to the list of dependencies in 95 | rebar.conf: 96 | 97 | ```Erlang 98 | {deps, [ 99 | {macaroons, "1.0.2", {git, "https://github.com/kzemek/macaroons.git", {tag, "1.0.2"}}} 100 | ]}. 101 | ``` 102 | 103 | Creating Your First Macaroon 104 | ---------------------------- 105 | 106 | Imagine that you ran a bank, and were looking to use macaroons as the basis of 107 | your authorization logic. Assuming you already installed libmacaroons, you can 108 | create a macaroon like this: 109 | 110 | ```Erlang 111 | 1> Secret = "this is our super secret key; only we should know it". 112 | 2> Public = "we used our secret key". 113 | 3> Location = "http://mybank/". 114 | 4> M = macaroon:create(Location, Secret, Public). 115 | ``` 116 | 117 | We've created our first macaroon! 118 | 119 | You can see here that it took three pieces of information to create a macaroon. 120 | We start with a secret. Here, we just have English text, but in reality we 121 | would want to use something more random and less predictable (see below for a 122 | suggestion on how to generate one). The public portion tells us which secret we 123 | used to create the macaroon, but doesn't give anyone else a clue as to the 124 | contents of the secret. Anyone in possession of the macaroon can see the public 125 | portion: 126 | 127 | ```Erlang 128 | 5> macaroon:identifier(M). 129 | <<"we used our secret key">> 130 | ``` 131 | 132 | This public portion, known as the macaroon's identifier, can be anything that 133 | enables us to remember our secret. In this example, we know that the string 'we 134 | used our secret key' always refers to this secret. We could just as easily keep 135 | a database mapping public macaroon identities to private secrets, or encrypt the 136 | public portion using a key known only to us. The only requirement here is that 137 | our application be able to remember the secret given the public portion, and 138 | that no one else is able to guess the secret. 139 | 140 | Our macaroon includes some additional information that enables us to add caveats 141 | and figure out where the macaroon should be used. The macaroon's location gives 142 | a hint to the user about where the macaroon is accepted for authorization. This 143 | hint is a free-form string maintained to help applications figure out where to 144 | use macaroons; the libmacaroons library (and by extension, the Erlang wrapper) 145 | does not ascribe any meaning to this location. 146 | 147 | Each macaroon also has a signature that is the key used to add caveats and 148 | verify the macaroon. The signature is computed by the macaroons library, and is 149 | unique to each macaroon. Applications should never need to directly work with 150 | the signature of the macaroon. Any entity in possession of the macaroon's 151 | signature should be assumed to possess the macaroon itself. 152 | 153 | Both of these pieces of information are publicly accessible: 154 | 155 | ```Erlang 156 | 6> macaroon:location(M). 157 | <<"http://mybank/">> 158 | 7> macaroon:signature(M). 159 | <<"e3d9e02908526c4c0039ae15114115d97fdd68bf2ba379b342aaf0f617d0552f">> 160 | ``` 161 | 162 | We can share this macaroon with others by serializing it. The serialized form 163 | is pure-ASCII, and is safe for inclusion in secure email, a standard HTTPS 164 | cookie, or a URL. We can get the serialized form with: 165 | 166 | ```Erlang 167 | 8> macaroon:serialize(M). 168 | {ok,<<"MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudGlmaWVyIHdlIHVzZWQgb3VyIHNlY3JldCBrZXkKMDAyZnNpZ25hdHVyZSDj"...>>} 169 | ``` 170 | 171 | Of course, this serialized form can be displayed in a more human-readable form 172 | for easy debugging: 173 | 174 | ```Erlang 175 | 9> io:format("~s", [macaroon:inspect(M)]). 176 | location http://mybank/ 177 | identifier we used our secret key 178 | signature e3d9e02908526c4c0039ae15114115d97fdd68bf2ba379b342aaf0f617d0552f 179 | ok 180 | ``` 181 | 182 | Adding Caveats 183 | -------------- 184 | 185 | At this point, we have an unconstrained macaroon that authorizes everything 186 | within our bank. In practice, such a macaroon is dangerous, because very few 187 | people should hold such power. Let's add a caveat to our macaroon that 188 | restricts it to just the account number 3735928559. 189 | 190 | ```Erlang 191 | 10> macaroon:add_first_party_caveat(M, "account = 3735928559"). 192 | ``` 193 | 194 | This new macaroon includes the same identifier and location that our old 195 | macaroon from our initial macaroon, but includes the additional caveat that 196 | restricts the bank account. The signature of this new macaroon is different, 197 | and incorporates the new caveat we've just added. An entity in possession of 198 | this new macaroon cannot simply remove our new caveat to construct the old 199 | macaroon: 200 | 201 | ```Erlang 202 | 11> M2 = macaroon:add_first_party_caveat(M, "account = 3735928559"). 203 | 12> io:format("~s", [macaroon:inspect(M2)]). 204 | location http://mybank/ 205 | identifier we used our secret key 206 | cid account = 3735928559 207 | signature 1efe4763f290dbce0c1d08477367e11f4eee456a64933cf662d79772dbb82128 208 | ok 209 | ``` 210 | 211 | Of course, we can add a few more caveats, and the macaroon's signature will 212 | change with each of them. 213 | 214 | ```Erlang 215 | 13> M3 = macaroon:add_first_party_caveat(M2, "time < 2020-01-01T00:00"). 216 | 14> macaroon:signature(M3). 217 | <<"b5f06c8c8ef92f6c82c6ff282cd1f8bd1849301d09a2db634ba182536a611c49">> 218 | 15> M4 = macaroon:add_first_party_caveat(M3, "email = alice@example.org"). 219 | 16> macaroon:signature(M4). 220 | <<"ddf553e46083e55b8d71ab822be3d8fcf21d6bf19c40d617bb9fb438934474b6">> 221 | 17> io:format("~s", [macaroon:inspect(M4)]). 222 | location http://mybank/ 223 | identifier we used our secret key 224 | cid account = 3735928559 225 | cid time < 2020-01-01T00:00 226 | cid email = alice@example.org 227 | signature ddf553e46083e55b8d71ab822be3d8fcf21d6bf19c40d617bb9fb438934474b6 228 | ok 229 | ``` 230 | 231 | The combination of all caveats in this macaroon authorize alice@example.org to 232 | access account 3735928559 until the end of 2019. Alice may present this 233 | macaroon to the bank any time she wishes to prove to the bank that she is 234 | authorized to access her account. Ideally, she'll transmit the serialized form 235 | of the macaroon to the bank: 236 | 237 | ```Erlang 238 | 18> {ok, Msg} = macaroon:serialize(M4). 239 | {ok,<<"MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudGlmaWVyIHdlIHVzZWQgb3VyIHNlY3JldCBrZXkKMDAxZGNpZCBhY2NvdW50"...>>} 240 | 19> % send msg to the bank 241 | ``` 242 | 243 | Verifying Macaroons 244 | ------------------- 245 | 246 | Our bank application's purpose is to protect users accounts from unauthorized 247 | access. For that reason, it cannot just accept anything that looks like a 248 | macaroon---that would defeat the point of using macaroons in the first place. 249 | So how can we ensure that only authorized users access the bank? 250 | 251 | We can determine whether a request is authorized through a process called 252 | verification. First, we construct a verifier that can determine whether the 253 | caveats on macaroons are satisfied. We can then use our verifier to determine 254 | whether a given macaroon is authorized in the context of the request. For 255 | example, our bank account application knows the account number specified in the 256 | request, and can specify `account = #` when building the verifier. The 257 | verifier can then check that this matches the information within the macaroon, 258 | and authorize the macaroon if it does indeed match. 259 | 260 | Let's walk through the verification process for Alice's macaroon that we 261 | constructed in the previous section. The first step, of course, is for the bank 262 | to deserialize the macaroon from the message. This converts the macaroon into a 263 | form we can work with. 264 | 265 | ```Erlang 266 | 1> {ok, M} = macaroon:deserialize(Msg). 267 | 2> io:format("~s", [macaroon:inspect(M)]). 268 | location http://mybank/ 269 | identifier we used our secret key 270 | cid account = 3735928559 271 | cid time < 2020-01-01T00:00 272 | cid email = alice@example.org 273 | signature ddf553e46083e55b8d71ab822be3d8fcf21d6bf19c40d617bb9fb438934474b6 274 | ok 275 | ``` 276 | 277 | We have the same macaroon that Alice believes authorizes her to access her own 278 | account, but we must verify this for ourselves. One (very flawed) way we could 279 | try to verify this macaroon would be to manually parse it and authorize the 280 | request if its caveats are true. But handling things this way completely 281 | sidesteps all the crypto-goodness that macaroons are built upon. 282 | 283 | Another approach to verification would be to use libmacaroons's built-in 284 | verifier to process the macaroon. The verifier hides many of the details of the 285 | verification process, and provides a natural way to work with many kinds of 286 | caveats. The verifier itself is constructed once, and may be re-used to verify 287 | multiple macaroons. 288 | 289 | ```Erlang 290 | 3> V = macaroon_verifier:create(). 291 | ``` 292 | 293 | Let's go ahead and try to verify the macaroon to see if the request is 294 | authorized. To verify the request, we need to provide the verifier with Alice's 295 | macaroon, and the secret that was used to construct it. In a real application, 296 | we would retrieve the secret using `macaroon:identifier(M)`; here, we know the 297 | secret and provide it directly. A verifier can only ever successfully verify 298 | the macaroon when provided with the macaroon and its corresponding secret---no 299 | secret, no authorization. 300 | 301 | Intuitively, our verifier should say that this macaroon is unauthorized because 302 | our verifier cannot prove that any of the caveats we've added are satisfied. We 303 | can see that it fails just as we would expect: 304 | 305 | ```Erlang 306 | 4> macaroon_verifier:verify(V, M, Secret). 307 | {error,{unverified_caveat,<<"account = 3735928559">>}} 308 | ``` 309 | 310 | We can inform the verifier of the caveats used by our application using two 311 | different techniques. The first technique is to directly provide the verifier 312 | with the caveats that match the context of the request. For example, every 313 | account-level action in a typical banking application is performed by a specific 314 | user and targets a specific account. This information is fixed for each 315 | request, and is known at the time of the request. We can tell the verifier 316 | directly about these caveats like so: 317 | 318 | ```Erlang 319 | 5> V1 = macaroon_verifier:satisfy_exact(V, "account = 3735928559"). 320 | 6> V2 = macaroon_verifier:satisfy_exact(V1, "email = alice@example.org"). 321 | ``` 322 | 323 | Caveats like these are called *exact caveats* because there is exactly one way 324 | to satisfy them. Either the account number is 3735928559, or it isn't. At 325 | verification time, the verifier will check each caveat in the macaroon against 326 | the list of satisfied caveats provided to `satisfyExact`. When it finds a 327 | match, it knows that the caveat holds and it can move onto the next caveat in 328 | the macaroon. 329 | 330 | Generally, you will specify multiple true statements as exact caveats, and let 331 | the verifier decide which are relevant to each macaroon at verification time. 332 | If you provide all exact caveats known to your application to the verifier, it 333 | becomes trivial to change policy decisions about authorization. The server 334 | performing authorization can treat the verifier as a black-box and does not need 335 | to change when changing the authorization policy. The actual policy is enforced 336 | when macaroons are minted and when caveats are embedded. In our banking 337 | example, we could provide some additional satisfied caveats to the verifier, 338 | to describe some (or all) of the properties that are known about the current 339 | request. In this manner, the verifier can be made more general, and be 340 | "future-proofed", so that it will still function correctly even if somehow 341 | the authorization policy for Alice changes; for example, by adding the three 342 | following facts, the verifier will continue to work even if Alice decides to 343 | self-attenuate her macaroons to be only usable from her IP address and browser: 344 | 345 | ```Erlang 346 | 7> V3 = macaroon_verifier:satisfy_exact(V2, "IP = 127.0.0.1"). 347 | 8> V4 = macaroon_verifier:satisfy_exact(V3, "browser = Chrome"). 348 | 9> V5 = macaroon_verifier:satisfy_exact(V4, "action = deposit"). 349 | ``` 350 | 351 | Although it's always possible to satisfy a caveat within a macaroon by providing 352 | it directly to the verifier, doing so can be quite tedious. Consider the caveat 353 | on access time embedded within Alice's macaroon. While an authorization routine 354 | could provide the exact caveat `time < 2020-01-01T00:00`, doing so would 355 | require inspecting the macaroon before building the verifier. Just like using 356 | MD5 to hash passwords, inspecting a macaroon's structure to build a verifier for 357 | it is considered to be very bad practice, and should be violently demonized in 358 | Hacker News discussions with vague, slightly inaccurate allusions to pbkdf2. 359 | 360 | So how can we tell our verifier that the caveat on access time is satisfied? We 361 | could provide many exact caveats of the form `time < YYYY-mm-ddTHH:MM`, but 362 | this reeks of inefficiency. The second technique for satisfying caveats provides 363 | a more general solution. 364 | 365 | Called *general caveats*, the second technique for informing the verifier that 366 | a caveat is satisfied allows for expressive caveats. Whereas exact caveats are 367 | checked by simple byte-wise equality, general caveats are checked using an 368 | application-provided callback that returns true if and only if the caveat is 369 | true within the context of the request. There's no limit on the contents of a 370 | general caveat, so long as the callback understands how to determine whether it 371 | is satisfied. 372 | 373 | We can verify the time caveat on Alice's macaroon by writing a function that 374 | checks the current time against the time specified by the caveat (the example 375 | uses [qdate] library for date operations): 376 | 377 | ```Erlang 378 | 10> CheckTime = 379 | 10> fun 380 | 10> (<<"time < ", Date/binary>>) -> 381 | 10> qdate:compare(erlang:now(), '<=', Date); 382 | 10> (_) -> 383 | 10> false 384 | 10> end. 385 | ``` 386 | 387 | This callback processes all caveats that begin with `time < `, and returns 388 | `true` if the specified time has not yet passed. We can see that our caveat 389 | does indeed return `true` when the caveat holds, and `false` otherwise: 390 | 391 | ```Erlang 392 | 11> CheckTime(<<"time < 2020-01-01T00:00">>). 393 | true 394 | 12> CheckTime(<<"time < 2014-01-01T00:00">>). 395 | false 396 | 13> CheckTime(<<"account = 3735928559">>). 397 | false 398 | ``` 399 | 400 | We can provide the `CheckTime` function directly to the verifier, so that it 401 | may check time-based predicates. 402 | 403 | ```Erlang 404 | 14> V6 = macaroon_verifier:satisfy_general(V5, CheckTime). 405 | ``` 406 | 407 | It's finally time to verify our macaroon! Now that we've informed the verifier 408 | of all the various caveats that our application could embed within a macaroon, 409 | we can expect that the verification step will succeed. 410 | 411 | ```Erlang 412 | 15> macaroon_verifier:verify(V6, M, Secret). 413 | ok 414 | ``` 415 | 416 | More importantly, the verifier will also work for macaroons we've not yet seen, 417 | like one that only permits Alice to deposit into her account: 418 | 419 | ```Erlang 420 | 16> N = macaroon:add_first_party_caveat(M, "action = deposit"). 421 | 17> macaroon_verifier:verify(V6, N, Secret). 422 | ok 423 | ``` 424 | 425 | Equally important is the verifier's ability to reject improper macaroons because 426 | they are not authorized. An improper macaroon could have additional caveats not 427 | known to the verifier, or have false general caveats. Worse, an unauthorized 428 | macaroon could be an attempt by a determined attacker to break into our bank. 429 | The verifier we've built will reject all of these scenarios: 430 | 431 | ```Erlang 432 | 18> % Unknown caveat 433 | 18> N2 = macaroon:add_first_party_caveat(M, "OS = Windows XP"). 434 | 19> macaroon_verifier:verify(V6, N2, Secret). 435 | {error,{unverified_caveat,<<"OS = Windows XP">>}} 436 | 20> 437 | 20> % False caveat 438 | 20> N3 = macaroon:add_first_party_caveat(M, "time < 2014-01-01T00:00"). 439 | 21> macaroon_verifier:verify(V6, N3, Secret). 440 | {error,{unverified_caveat,<<"time < 2020-01-01T00:00">>}} 441 | 22> 442 | 22> % Bad secret 443 | 22> macaroon_verifier:verify(V6, M, "this is not the secret we were looking for"). 444 | {error,{bad_signature_for_macaroon,<<"we used our secret key">>}} 445 | 23> 446 | 23> % Incompetent hackers trying to change the signature 447 | 23> {ok, N4} = macaroon:deserialize(<<"MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudGlmaWVyIHdlIHVzZWQgb3VyIHNl\nY3JldCBrZXkKMDAxZGNpZCBhY2NvdW50ID0gMzczNTkyODU1OQowMDIwY2lkIHRpbWUgPCAyMDIw\nLTAxLTAxVDAwOjAwCjAwMjJjaWQgZW1haWwgPSBhbGljZUBleGFtcGxlLm9yZwowMDJmc2lnbmF0\ndXJlID8f19FL+bkC9p/aoMmIecC7GxdOcLVyUnrv6lJMM7NSCg==\n">>). 448 | 24> io:format("~s", [macaroon:inspect(N4)]). 449 | location http://mybank/ 450 | identifier we used our secret key 451 | cid account = 3735928559 452 | cid time < 2020-01-01T00:00 453 | cid email = alice@example.org 454 | signature 3f1fd7d14bf9b902f69fdaa0c98879c0bb1b174e70b572527aefea524c33b352 455 | ok 456 | 25> macaroon:signature(M) =:= macaroon:signature(N4). 457 | false 458 | 26> macaroon_verifier:verify(V6, N4, Secret). 459 | {error,{bad_signature_for_macaroon,<<"we used our secret key">>}} 460 | ``` 461 | 462 | Using Third-Party Caveats 463 | ------------------------- 464 | 465 | Like first-party caveats, third-party caveats restrict the context in which a 466 | macaroon is authorized, but with a different form of restriction. Where a 467 | first-party caveat is checked directly within the verifier, a third-party caveat 468 | is checked by the third-party, who provides a discharge macaroon to prove that 469 | the original third-party caveat is true. The discharge macaroon is recursively 470 | inspected by the verifier; if it verifies successfully, the discharge macaroon 471 | serves as a proof that the original third-party caveat is satisfied. Of course, 472 | nothing stops discharge macaroons from containing embedded first- or third-party 473 | caveats for the verifier to consider during verification. 474 | 475 | Let's rework the above example to provide Alice with access to her account only 476 | after she authenticates with a service that is separate from the service 477 | processing her banking transactions. 478 | 479 | As before, we'll start by constructing a new macaroon with the caveat that is 480 | limited to Alice's bank account. 481 | 482 | ```Erlang 483 | 1> Secret = "this is a different super-secret key; never use the same secret twice". 484 | 2> Public = "we used our other secret key". 485 | 3> Location = "http://mybank/". 486 | 4> M = macaroon:create(Location, Secret, Public). 487 | 5> M2 = macaroon:add_first_party_caveat(M, "account = 3735928559"). 488 | 6> io:format("~s", [macaroon:inspect(M2)]). 489 | location http://mybank/ 490 | identifier we used our other secret key 491 | cid account = 3735928559 492 | signature 1434e674ad84fdfdc9bc1aa00785325c8b6d57341fc7ce200ba4680c80786dda 493 | ok 494 | ``` 495 | 496 | So far, so good. Now let's add a third party caveat to this macaroon that 497 | requires that Alice authenticate with `http://auth.mybank/` before being 498 | authorized access to her account. To add a third-party caveat we'll need to 499 | specify a location hint, generate a secret, and somehow be able to identify this 500 | secret later. Like the location used when creating a macaroon, the location 501 | used when adding a third-party caveat is simply a hint to the application that 502 | is uninterpreted by libmacaroons. 503 | 504 | The secret and identity are handled differently than during creation, however, 505 | because they need to be known to the third party. To do this, we'll provide the 506 | third-party with a randomly generated `CaveatKey` and the predicate we wish 507 | for it to check. In response, it will give us an identifier that we can embed 508 | within the macaroon. Later, when we need to construct a discharge macaroon, we 509 | can provide it with just this identifier, from which it can recall the key and 510 | predicate to check. 511 | 512 | Here's what this looks like in code: 513 | 514 | ```Erlang 515 | 7> % you'll likely want to use a higher entropy source to generate this key 516 | 7> CaveatKey = "4; guaranteed random by a fair toss of the dice". 517 | 8> Predicate = "user = Alice". 518 | 9> % send_to_auth(CaveatKey, Predicate). 519 | 9> % Identifier = recv_from_auth(). 520 | 9> Identifier = "this was how we remind auth of key/pred". 521 | 10> M3 = macaroon:add_third_party_caveat(M2, "http://auth.mybank/", CaveatKey, Identifier). 522 | 11> io:format("~s", [macaroon:inspect(M3)]). 523 | location http://mybank/ 524 | identifier we used our other secret key 525 | cid account = 3735928559 526 | cid this was how we remind auth of key/pred 527 | vid f678186d92de0589225da7827ed26509ed33eb83601b9e96b21cdee80f4a9834176b77f06f3fc3b59a1547231362797158ca61226681a7946fa2781bef9a95e746f52e91af74f9ae 528 | cl http://auth.mybank/ 529 | signature 9a986629809a40171d4cc32c3814fbf9e0d82f0b1eebb781fc3675d204e6066a 530 | ok 531 | ``` 532 | 533 | We now have a macaroon with a third-party caveat. The most interesting thing to 534 | note about this macaroon is that it includes no information that reveals the 535 | predicate it encodes. The third-party service knows the key and the predicate, 536 | and internally associates them with the identifier, but the identifier itself is 537 | arbitrary and betrays no information about the predicate to others. The service 538 | at `http://auth.mybank/` can authenticate that the user is Alice, and provide 539 | proof that the caveat is satisfied, without revealing Alice's identity. Other 540 | services can verify M and its associated discharge macaroon, without knowing the 541 | predicates the third-parties verified. 542 | 543 | The process for discharging third party caveats starts with the holder of the 544 | initial root macaroon, Alice. Alice looks at the macaroon for the list of third 545 | party caveat (location, identifier) pairs that must be addressed. 546 | 547 | ```Erlang 548 | 12> macaroon:third_party_caveats(M3). 549 | [{<<"http://auth.mybank/">>, 550 | <<"this was how we remind auth of key/pred">>}] 551 | ``` 552 | 553 | In a real application, we'd look at these third party caveats, and contact each 554 | location to retrieve the requisite discharge macaroons. We would include the 555 | identifier for the caveat in the request itself, so that the server can recall 556 | the secret used to create the third-party caveat. The server can then generate 557 | and return a new macaroon that discharges the caveat: 558 | 559 | ```Erlang 560 | 13> D = macaroon:create("http://auth.mybank/", CaveatKey, Identifier). 561 | 14> D2 = macaroon:add_first_party_caveat(D, "time < 2020-01-01T00:00"). 562 | 15> io:format("~s", [macaroon:inspect(D2)]). 563 | location http://auth.mybank/ 564 | identifier this was how we remind auth of key/pred 565 | cid time < 2020-01-01T00:00 566 | signature 2ed1049876e9d5840950274b579b0770317df54d338d9d3039c7c67d0d91d63c 567 | ok 568 | ``` 569 | 570 | This new macaroon enables the verifier to determine that the third party caveat 571 | is satisfied. Our target service added a time-limiting caveat to this macaroon 572 | that ensures that this discharge macaroon does not last forever. This ensures 573 | that Alice (or, at least someone authenticated as Alice) cannot use the 574 | discharge macaroon indefinitely and will eventually have to re-authenticate. 575 | 576 | Once Alice has both the root macaroon and the discharge macaroon in her 577 | possession, she can make the request to the target service. Making a request 578 | with discharge macaroons is only slightly more complicated than making requests 579 | with a single macaroon. In addition to serializing and transmitting all 580 | involved macaroons, there is preparation step that binds the discharge macaroons 581 | to the root macaroon. This binding step ensures that the discharge macaroon is 582 | useful only when presented alongside the root macaroon. The root macaroon is 583 | used to bind the discharge macaroons like this: 584 | 585 | ```Erlang 586 | 16> DP = macaroon:prepare_for_request(M3, D2). 587 | ``` 588 | 589 | If we were to look at the signatures on these prepared discharge macaroons, we 590 | would see that the binding process has irreversibly altered their signature(s). 591 | 592 | ```Erlang 593 | 17> macaroon:signature(D2). 594 | <<"2ed1049876e9d5840950274b579b0770317df54d338d9d3039c7c67d0d91d63c">> 595 | 18> macaroon:signature(DP). 596 | <<"dd3690773cd08867dc544d7a84a26aae2581f56a21f77f886812865abdea9554">> 597 | ``` 598 | 599 | The root macaroon `M3` and its discharge macaroons `DP` are ready for the 600 | request. Alice can serialize them all and send them to the bank to prove she is 601 | authorized to access her account. The bank can verify them using the same 602 | verifier we built before. We provide the discharge macaroons as a third 603 | argument to the verify call: 604 | 605 | ```Erlang 606 | 19> macaroon_verifier:verify(V6, M3, Secret, [DP]). 607 | ok 608 | ``` 609 | 610 | Without the `prepareForRequest` call, the verification would fail: 611 | 612 | ```Erlang 613 | 20> macaroon_verifier:verify(V6, M3, Secret, [D2]). 614 | {error,{bad_signature_for_macaroon,<<"this was how we remind auth of key/pred">>}} 615 | ``` 616 | 617 | Choosing Secrets 618 | ---------------- 619 | 620 | For clarity, we've generated human-readable secrets that we use as the root keys 621 | of all of our macaroons. In practice, this is terribly insecure and can lead to 622 | macaroons that can easily be forged because the secret is too predictable. To 623 | avoid this, we recommend generating secrets using a sufficient number of 624 | suitably random bytes. Because the bytes are a secret key, they should be drawn 625 | from a source with enough entropy to ensure that the key cannot be guessed 626 | before the macaroon falls out of use. 627 | 628 | The `macaroon` module exposes a constant that is the ideal number of bytes 629 | these secret keys should contain. Any shorter is wasting an opportunity for 630 | security. 631 | 632 | ```Erlang 633 | 1> macaroon:suggested_secret_length(). 634 | 32 635 | ``` 636 | 637 | Third-Party Caveats with Public Keys 638 | ------------------------------------ 639 | 640 | Public key cryptography can enable much more efficient schemes for adding 641 | third-party caveats. In the above example where we added a third-party caveat, 642 | the caveat's identifier was generated by the third party and retrieved with in 643 | one round trip. We can eliminate the round trip when the third party has a 644 | well-known public key. We can encrypt the caveat's secret, and the predicate to 645 | be checked using this public key, and use the ciphertext directly as the 646 | caveat's identifier. This saves a round trip, and frees the third party from 647 | having to remember an association between identifiers and key/predicate pairs. 648 | 649 | [rescrv/libmacaroons]: https://github.com/rescrv/libmacaroons/blob/ca1e875b06862e6cd515a4e3c0e5b124ef502366/README 650 | [libmacaroons]: https://github.com/rescrv/libmacaroons 651 | [macaroons]: http://research.google.com/pubs/pub41892.html 652 | [qdate]: https://github.com/choptastic/qdate 653 | [libsodium]: https://github.com/jedisct1/libsodium 654 | [rebar]: https://rebar3.org 655 | [enacl]: https://github.com/jlouis/enacl 656 | --------------------------------------------------------------------------------