From 2987afc3c9260ceae046dd6b141a57d40547d2d6 Mon Sep 17 00:00:00 2001 From: jurraca Date: Sun, 9 Jul 2023 00:04:24 +0200 Subject: [PATCH 1/7] initial sketch of v2 macaroon impl --- lib/macaroon_v2.ex | 73 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 lib/macaroon_v2.ex diff --git a/lib/macaroon_v2.ex b/lib/macaroon_v2.ex new file mode 100644 index 0000000..be7b85b --- /dev/null +++ b/lib/macaroon_v2.ex @@ -0,0 +1,73 @@ +defmodule MacaroonV2 do + import Bitwise + @field_types %{location: 1, identifier: 2, vid: 4, signature: 6} + + def encode_field_type(field_type) do + encode_varuint64(field_type) + end + + def encode_field_length(length) do + encode_varuint64(length) + end + + def encode_varuint64(num) do + encode_varuint64(num, <<>>) + end + + defp encode_varuint64(0x80, enc), do: enc + + defp encode_varuint64(num, encoded) when num > 0x80 do + new_encoded = encoded <> (num |> band(0xFF) |> bor(0x80)) + new_num = num >>> 7 + encode_varuint64(new_num, new_encoded) + end + + defp encode_varuint64(num, _) do + <> + end + + def encode_field_content(content) do + content + end + + def encode_macaroon(version, location, identifier, caveats, signature) do + version_bytes = <> + location_bytes = encode_field(@field_types.location, location) + identifier_bytes = encode_field(@field_types.identifier, identifier) + caveats_bytes = encode_caveats(caveats) + signature_bytes = encode_field(@field_types.signature, signature) + + version_bytes <> + location_bytes <> identifier_bytes <> <<0>> <> caveats_bytes <> <<0>> <> signature_bytes + end + + def encode_caveats(caveats) do + Enum.map(caveats, &encode_caveat/1) + |> List.flatten() + |> Enum.join() + end + + def encode_caveat([]), do: [<<>>] + + def encode_caveat(caveat) do + location_bytes = encode_field(@field_types.location, caveat.location) + identifier_bytes = encode_field(@field_types.identifier, caveat.identifier) + vid_bytes = encode_optional_field(@field_types.vid, caveat.vid) + + location_bytes <> identifier_bytes <> vid_bytes + end + + def encode_field(field_type, field_content) do + encoded_field_type = encode_field_type(field_type) + encoded_field_length = encode_field_length(byte_size(field_content)) + encoded_field_content = encode_field_content(field_content) + + encoded_field_type <> encoded_field_length <> encoded_field_content + end + + def encode_optional_field(_, nil), do: <<>> + + def encode_optional_field(field_type, field_content) do + encode_field(field_type, field_content) + end +end From 6680d094329b7296d59d2b2e17a2900e7b9c6748 Mon Sep 17 00:00:00 2001 From: jurraca Date: Sun, 16 Jul 2023 18:47:19 +0200 Subject: [PATCH 2/7] Add initial macaroon V2 impl --- lib/macaroon_v2.ex | 249 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 210 insertions(+), 39 deletions(-) diff --git a/lib/macaroon_v2.ex b/lib/macaroon_v2.ex index be7b85b..bd60105 100644 --- a/lib/macaroon_v2.ex +++ b/lib/macaroon_v2.ex @@ -1,73 +1,244 @@ defmodule MacaroonV2 do + @doc """ + Encode and decode Macaroons for the version 2 spec. + See https://github.com/rescrv/libmacaroons/blob/master/doc/format.txt + + Macaroons are a TLV encoding using variable length integers to encode length. + """ import Bitwise - @field_types %{location: 1, identifier: 2, vid: 4, signature: 6} + require Logger + + @field_types %{location: 1, identifier: 2, vid: 4, signature: 6, eos: 0} - def encode_field_type(field_type) do - encode_varuint64(field_type) + def encode_macaroon(version, location, identifier, caveats, signature) do + version_bytes = <> + location_bytes = encode_field(@field_types.location, location) + identifier_bytes = encode_field(@field_types.identifier, identifier) + caveats_bytes = encode_caveats(caveats) + signature_bytes = encode_field(@field_types.signature, signature) + + version_bytes <> + location_bytes <> + identifier_bytes <> + <<@field_types.eos>> <> caveats_bytes <> <<@field_types.eos>> <> signature_bytes end - def encode_field_length(length) do - encode_varuint64(length) + def encode_field(field_type, field_content) when is_binary(field_content) do + encode_packet(field_type, byte_size(field_content), field_content) end - def encode_varuint64(num) do - encode_varuint64(num, <<>>) + def encode_packet(field_type, len, field_content) do + encoded_field_type = encode_varuint64(field_type) + encoded_field_length = encode_varuint64(len) + + encoded_field_type <> encoded_field_length <> field_content end - defp encode_varuint64(0x80, enc), do: enc + def encode_varuint64(num) when num < 0, + do: {:error, "Integer must be positive, got #{Integer.to_string(num)}"} + + def encode_varuint64(num) when is_integer(num) and num < 0x80, do: <> + def encode_varuint64(num) when num >= 0x80, do: encode_varuint64(<<>>, num) - defp encode_varuint64(num, encoded) when num > 0x80 do - new_encoded = encoded <> (num |> band(0xFF) |> bor(0x80)) + def encode_varuint64(data, num) do + new_encoded = num |> band(0x7F) new_num = num >>> 7 - encode_varuint64(new_num, new_encoded) - end - defp encode_varuint64(num, _) do - <> + if new_num == 0 do + data <> <> + else + new_data = data <> <> + encode_varuint64(new_data, new_num) + end end - def encode_field_content(content) do - content - end + def encode_caveat([]), do: [<<>>] - def encode_macaroon(version, location, identifier, caveats, signature) do - version_bytes = <> - location_bytes = encode_field(@field_types.location, location) + def encode_caveat(%{identifier: identifier} = caveat) do + location_bytes = encode_optional_location(caveat) identifier_bytes = encode_field(@field_types.identifier, identifier) - caveats_bytes = encode_caveats(caveats) - signature_bytes = encode_field(@field_types.signature, signature) + vid_bytes = encode_optional_vid(caveat) - version_bytes <> - location_bytes <> identifier_bytes <> <<0>> <> caveats_bytes <> <<0>> <> signature_bytes + location_bytes <> identifier_bytes <> vid_bytes <> <<@field_types.eos>> + end + + def encode_caveat(_caveat) do + {:error, "Caveat missing a required attribute: 'identifier'"} end def encode_caveats(caveats) do - Enum.map(caveats, &encode_caveat/1) + caveats + |> Enum.map(&encode_caveat/1) |> List.flatten() |> Enum.join() end - def encode_caveat([]), do: [<<>>] + defp encode_optional_location(%{location: location}), + do: encode_field(@field_types.location, location) + + defp encode_optional_location(_), do: <<>> + defp encode_optional_vid(%{vid: vid}), do: encode_field(@field_types.vid, vid) + defp encode_optional_vid(_), do: <<>> - def encode_caveat(caveat) do - location_bytes = encode_field(@field_types.location, caveat.location) - identifier_bytes = encode_field(@field_types.identifier, caveat.identifier) - vid_bytes = encode_optional_field(@field_types.vid, caveat.vid) +############ Decode - location_bytes <> identifier_bytes <> vid_bytes + def decode_mac(binary) do + # first we pick off the version + <<_v::size(8), rest::binary>> = binary + # then decode the rest + decode_fields(rest, %{}) end - def encode_field(field_type, field_content) do - encoded_field_type = encode_field_type(field_type) - encoded_field_length = encode_field_length(byte_size(field_content)) - encoded_field_content = encode_field_content(field_content) + # nothing left to decode, return decoded Macaroon + def decode_fields(<<>>, mac), do: {:ok, mac} + # a zero byte is a stop byte (field type EOS). + # The first zero byte indicates caveats begin; a second zero byte indicates the end of caveats. + def decode_fields(<<0::size(8), rest::binary>>, %{caveats: caveats} = mac) do + case Enum.count(caveats) > 0 do + true -> + Logger.info("hit 0 zero byte") + decode_fields(rest, mac) + + false -> + Logger.info("End of caveats but they're empty") + decode_fields(rest, mac) + end + end + + def decode_fields(<<0::size(8), rest::binary>>, mac) do + <> = rest + + if first == 0 do + Logger.info("no caveats found") + decode_fields(data, mac) + else + # handle caveats + new_mac = Map.put(mac, :caveats, []) + decode_caveats(rest, new_mac, %{}) + end + end + + def decode_fields(data, mac) do + with {:ok, {field_name, val}, rest} <- decode_field(data) do + mac = Map.put(mac, field_name, val) + IO.inspect(mac) + decode_fields(rest, mac) + else + err -> err + end + end + + def decode_field(data) when is_binary(data) do + with <> <- data, + {:ok, field_name} <- get_field_name(field_type) do + {:ok, len, bytes_read} = decode_len(lv) + IO.inspect(len, label: "len") + {:ok, val, rest} = decode_value(lv, bytes_read, bytes_read + len) + {:ok, {field_name, val}, rest} + end + end + + @doc """ + Caveats are recursively accumulated, until a single zero byte denotes the end of caveats. + At which point we return to decoding regular fields. + """ + def decode_caveats(<<0::size(8), rest::binary>>, %{caveats: _} = mac, _acc) do + decode_fields(rest, mac) + end + + def decode_caveats(bin, %{caveats: cavs} = mac, acc) do + Logger.info("Entering caveat loop") + + with {:ok, {field_name, val}, rest} <- decode_field(bin) do + new_acc = Map.put(acc, field_name, val) + + # if the next byte is zero, the caveat section is ended, we add it and process the next caveat. + # if its not zero, there is more to decode for this section + case first_byte_zero?(rest) do + true -> + new_mac = add_caveat_section(mac, new_acc) - encoded_field_type <> encoded_field_length <> encoded_field_content + rest + |> binary_slice(1..-1//1) + |> decode_caveats(new_mac, %{}) + + false -> + decode_caveats(rest, mac, new_acc) + end + end + end + + def add_caveat_section(%{caveats: caveats} = mac, section) do + Map.put(mac, :caveats, caveats ++ [section]) + end + + def decode_len(<> = lv) do + if len < 0x80 do + {:ok, len, 1} + else + decode_varuint64(lv) + end + end + + def decode_value(data, read_start, read_end) do + # inclusive + val = binary_part(data, read_start, read_end - 1) + # read to the end + rest = binary_slice(data, read_end..-1//1) + {:ok, val, rest} + end + + def decode_varuint64(data) do + case parse_varint(data, 0, 0) do + {:ok, val, bytes_read} -> + bytes_left = binary_slice(data, 0..bytes_read) + {:ok, val, bytes_left} + + msg -> + {:error, msg} + end end - def encode_optional_field(_, nil), do: <<>> + def parse_varint(<>, acc, bytes_read) do + # python + # n = 0 + # shift = 0 + # i = 0 + # for b in data: + # b = ord(b) + # i += 1 + # if b < 0x80: + # return n | b << shift, i + # n |= (b & 0x7f) << shift + # shift += 7 + + cond do + b < 0x80 -> + # this is the last byte of the varint encoding + # it can be directly added to the result x by shifting it s bits to the left. + {:ok, bor(acc, b <<< 7), bytes_read + 1} + + true -> + # the value of the byte is extracted by performing a bitwise AND operation with 0x7f (127 in decimal), + # and then shifted 7 bits to the left and ORed with the accumulated result x. + acc = acc ||| band(b, 0x7F) <<< 7 + parse_varint(data, acc, bytes_read + 1) + end + end - def encode_optional_field(field_type, field_content) do - encode_field(field_type, field_content) + @doc """ + If the Type field of the TLV section is a standard one, use it. + If not, use the integer code as the name. + """ + defp get_field_name(int) do + if int in Map.values(@field_types) do + [{field_name, _}] = Enum.filter(@field_types, fn {_k, v} -> v == int end) + {:ok, field_name} + else + {:ok, Integer.to_string(int)} + end end + + defp first_byte_zero?(<<0::size(8), _::binary>>), do: true + defp first_byte_zero?(_), do: false end From a9f7e153e9defd378254670eefdd68d90c63550c Mon Sep 17 00:00:00 2001 From: jurraca Date: Tue, 18 Jul 2023 16:17:05 +0200 Subject: [PATCH 3/7] Encode / Decode working, begin matching up with existing API --- lib/macaroon.ex | 4 + .../binary_v2.ex} | 150 +++++++++--------- 2 files changed, 83 insertions(+), 71 deletions(-) rename lib/{macaroon_v2.ex => serializers/binary_v2.ex} (61%) diff --git a/lib/macaroon.ex b/lib/macaroon.ex index aa28347..6b77d01 100644 --- a/lib/macaroon.ex +++ b/lib/macaroon.ex @@ -151,6 +151,10 @@ defmodule Macaroon do Binary.encode(macaroon, :v1) end + def serialize_v2(%Types.Macaroon{} = macaroon, :binary) do + Binary.V2.encode(macaroon) + end + @doc """ Deserializes a JSON or Base64 serialized Macaroon string diff --git a/lib/macaroon_v2.ex b/lib/serializers/binary_v2.ex similarity index 61% rename from lib/macaroon_v2.ex rename to lib/serializers/binary_v2.ex index bd60105..516790c 100644 --- a/lib/macaroon_v2.ex +++ b/lib/serializers/binary_v2.ex @@ -1,26 +1,38 @@ -defmodule MacaroonV2 do - @doc """ - Encode and decode Macaroons for the version 2 spec. - See https://github.com/rescrv/libmacaroons/blob/master/doc/format.txt - - Macaroons are a TLV encoding using variable length integers to encode length. +defmodule Macaroon.Serializers.Binary.V2 do + @moduledoc """ + Version 2 Macaroon binary encoding and decoding. + See format [doc](https://github.com/rescrv/libmacaroons/blob/master/doc/format.txt) for spec. """ + import Bitwise - require Logger + alias Macaroon.Types.{Macaroon, Caveat} - @field_types %{location: 1, identifier: 2, vid: 4, signature: 6, eos: 0} + @field_types %{location: 1, public_identifier: 2, vid: 4, signature: 6, eos: 0} + + def encode(%Macaroon{ + public_identifier: identifier, + location: location, + signature: signature, + caveats: caveats + }) do + encode_macaroon(2, location, identifier, caveats, signature) + end def encode_macaroon(version, location, identifier, caveats, signature) do - version_bytes = <> - location_bytes = encode_field(@field_types.location, location) - identifier_bytes = encode_field(@field_types.identifier, identifier) - caveats_bytes = encode_caveats(caveats) - signature_bytes = encode_field(@field_types.signature, signature) + with {:ok, location} <- encode_field(@field_types.location, location), + {:ok, id} <- encode_field(@field_types.public_identifier, identifier), + {:ok, sig} <- encode_field(@field_types.signature, signature), + {:ok, caveats_encoded} <- encode_caveats(caveats) do - version_bytes <> - location_bytes <> - identifier_bytes <> - <<@field_types.eos>> <> caveats_bytes <> <<@field_types.eos>> <> signature_bytes + version = <> + encoded_string = + (version <> location <> id <> <<@field_types.eos>> <> caveats_encoded <> <<@field_types.eos>> <> sig) + |> Base.url_encode64(padding: false) + + {:ok, encoded_string} + else + {:error, _} = err -> err + end end def encode_field(field_type, field_content) when is_binary(field_content) do @@ -31,7 +43,7 @@ defmodule MacaroonV2 do encoded_field_type = encode_varuint64(field_type) encoded_field_length = encode_varuint64(len) - encoded_field_type <> encoded_field_length <> field_content + {:ok, encoded_field_type <> encoded_field_length <> field_content} end def encode_varuint64(num) when num < 0, @@ -52,87 +64,94 @@ defmodule MacaroonV2 do end end + def encode_caveats(caveats) do + encoded = caveats + |> Enum.map(&encode_caveat/1) + |> List.flatten() + |> Enum.join() + + {:ok, encoded} + end + def encode_caveat([]), do: [<<>>] - def encode_caveat(%{identifier: identifier} = caveat) do - location_bytes = encode_optional_location(caveat) - identifier_bytes = encode_field(@field_types.identifier, identifier) - vid_bytes = encode_optional_vid(caveat) + def encode_caveat(%Caveat{} = caveat) do + {:ok, location_bytes} = encode_optional_location(caveat) + {:ok, identifier_bytes} = encode_field(@field_types.public_identifier, caveat.caveat_id) + {:ok, vid_bytes} = encode_optional_vid(caveat) location_bytes <> identifier_bytes <> vid_bytes <> <<@field_types.eos>> end def encode_caveat(_caveat) do - {:error, "Caveat missing a required attribute: 'identifier'"} - end - - def encode_caveats(caveats) do - caveats - |> Enum.map(&encode_caveat/1) - |> List.flatten() - |> Enum.join() + {:error, "Caveat must be of type Caveat."} end - defp encode_optional_location(%{location: location}), + defp encode_optional_location(%{location: location}) when not is_nil(location), do: encode_field(@field_types.location, location) - defp encode_optional_location(_), do: <<>> + defp encode_optional_location(_), do: {:ok, <<>>} defp encode_optional_vid(%{vid: vid}), do: encode_field(@field_types.vid, vid) - defp encode_optional_vid(_), do: <<>> - -############ Decode + defp encode_optional_vid(_), do: {:ok, <<>>} + @doc """ + Decode a Base64 encoded macaroon. + """ def decode_mac(binary) do + {:ok, decoded} = Base.url_decode64(binary, padding: false) # first we pick off the version - <<_v::size(8), rest::binary>> = binary + <> = decoded # then decode the rest - decode_fields(rest, %{}) + decode_fields(rest, %{version: v}) end - # nothing left to decode, return decoded Macaroon + @doc """ + Decode fields of binary macaroon. + Fields are recursively accumulated: + - if the accumulator is empty, return the decoded macaroon. + - Field Type EOS (a zero byte) delimits the caveats section, and individual caveats within that section. + """ def decode_fields(<<>>, mac), do: {:ok, mac} - # a zero byte is a stop byte (field type EOS). - # The first zero byte indicates caveats begin; a second zero byte indicates the end of caveats. + def decode_fields(<<0::size(8), rest::binary>>, %{caveats: caveats} = mac) do case Enum.count(caveats) > 0 do true -> - Logger.info("hit 0 zero byte") decode_fields(rest, mac) false -> - Logger.info("End of caveats but they're empty") decode_fields(rest, mac) end end + ## Decoding + def decode_fields(<<0::size(8), rest::binary>>, mac) do <> = rest + # If there is a second zero byte, this means the caveats section is empty. + # Proceed to decoding the rest of the fields. + # Else, decode the caveats. if first == 0 do - Logger.info("no caveats found") decode_fields(data, mac) else - # handle caveats new_mac = Map.put(mac, :caveats, []) decode_caveats(rest, new_mac, %{}) end end def decode_fields(data, mac) do - with {:ok, {field_name, val}, rest} <- decode_field(data) do + with {:ok, {field_name, val}, rest} <- decode_packet(data) do mac = Map.put(mac, field_name, val) - IO.inspect(mac) decode_fields(rest, mac) else err -> err end end - def decode_field(data) when is_binary(data) do + def decode_packet(data) when is_binary(data) do with <> <- data, {:ok, field_name} <- get_field_name(field_type) do {:ok, len, bytes_read} = decode_len(lv) - IO.inspect(len, label: "len") {:ok, val, rest} = decode_value(lv, bytes_read, bytes_read + len) {:ok, {field_name, val}, rest} end @@ -140,19 +159,17 @@ defmodule MacaroonV2 do @doc """ Caveats are recursively accumulated, until a single zero byte denotes the end of caveats. - At which point we return to decoding regular fields. """ def decode_caveats(<<0::size(8), rest::binary>>, %{caveats: _} = mac, _acc) do decode_fields(rest, mac) end - def decode_caveats(bin, %{caveats: cavs} = mac, acc) do - Logger.info("Entering caveat loop") - - with {:ok, {field_name, val}, rest} <- decode_field(bin) do + def decode_caveats(bin, %{caveats: _} = mac, acc) do + with {:ok, {field_name, val}, rest} <- decode_packet(bin) do new_acc = Map.put(acc, field_name, val) - # if the next byte is zero, the caveat section is ended, we add it and process the next caveat. + # if the next byte is zero, the caveat section is ended. + # we add it and process the next caveat. # if its not zero, there is more to decode for this section case first_byte_zero?(rest) do true -> @@ -180,8 +197,11 @@ defmodule MacaroonV2 do end end + @doc """ + Read a value from a binary based on index. + Return the rest of the binary. + """ def decode_value(data, read_start, read_end) do - # inclusive val = binary_part(data, read_start, read_end - 1) # read to the end rest = binary_slice(data, read_end..-1//1) @@ -199,19 +219,11 @@ defmodule MacaroonV2 do end end + @doc """ + Parses a variable length int. + Reference Python impl: https://github.com/ecordell/pymacaroons/blob/master/pymacaroons/serializers/binary_serializer.py#L315 + """ def parse_varint(<>, acc, bytes_read) do - # python - # n = 0 - # shift = 0 - # i = 0 - # for b in data: - # b = ord(b) - # i += 1 - # if b < 0x80: - # return n | b << shift, i - # n |= (b & 0x7f) << shift - # shift += 7 - cond do b < 0x80 -> # this is the last byte of the varint encoding @@ -226,10 +238,6 @@ defmodule MacaroonV2 do end end - @doc """ - If the Type field of the TLV section is a standard one, use it. - If not, use the integer code as the name. - """ defp get_field_name(int) do if int in Map.values(@field_types) do [{field_name, _}] = Enum.filter(@field_types, fn {_k, v} -> v == int end) From eb851effa11b5cfae85a88e3e90674bcb008fcfc Mon Sep 17 00:00:00 2001 From: jurraca Date: Tue, 18 Jul 2023 22:18:32 +0200 Subject: [PATCH 4/7] add version to Macaroon type, fix caveat decoding: handle caveat_id, party --- lib/serializers/binary_v2.ex | 14 +++++++++++--- lib/types/macaroon.ex | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/serializers/binary_v2.ex b/lib/serializers/binary_v2.ex index 516790c..27a0b2e 100644 --- a/lib/serializers/binary_v2.ex +++ b/lib/serializers/binary_v2.ex @@ -102,7 +102,7 @@ defmodule Macaroon.Serializers.Binary.V2 do # first we pick off the version <> = decoded # then decode the rest - decode_fields(rest, %{version: v}) + decode_fields(rest, Macaroon.build([version: v])) end @doc """ @@ -119,7 +119,7 @@ defmodule Macaroon.Serializers.Binary.V2 do decode_fields(rest, mac) false -> - decode_fields(rest, mac) + decode_caveats(rest, mac, %{}) end end @@ -186,7 +186,15 @@ defmodule Macaroon.Serializers.Binary.V2 do end def add_caveat_section(%{caveats: caveats} = mac, section) do - Map.put(mac, :caveats, caveats ++ [section]) + party = if :location in Map.keys(section), do: :third, else: :first + + formatted = section + |> Map.put(:party, party) + |> Map.put(:caveat_id, section.public_identifier) + |> Enum.into([]) + |> Caveat.build() + + Map.put(mac, :caveats, caveats ++ [formatted]) end def decode_len(<> = lv) do diff --git a/lib/types/macaroon.ex b/lib/types/macaroon.ex index 12030dd..b641940 100644 --- a/lib/types/macaroon.ex +++ b/lib/types/macaroon.ex @@ -12,6 +12,7 @@ defmodule Macaroon.Types.Macaroon do field(:public_identifier, String.t(), enforce: true, default: "") field(:signature, String.t(), enforce: true, default: nil) field(:caveats, list(Caveat.t()), enforce: true, default: []) + field(:version, Integer.t(), enforce: true, default: nil) end struct_builder() From 637122de88b47cae9047672fc862f32fc919769e Mon Sep 17 00:00:00 2001 From: jurraca Date: Thu, 3 Aug 2023 23:44:17 +0200 Subject: [PATCH 5/7] simplify version handling: pass it explicitly on macaroon creation and decoding --- lib/macaroon.ex | 17 +++++++++-------- lib/serializers/binary.ex | 15 +++++++++++---- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/macaroon.ex b/lib/macaroon.ex index 6b77d01..6a2dd96 100644 --- a/lib/macaroon.ex +++ b/lib/macaroon.ex @@ -12,8 +12,8 @@ defmodule Macaroon do @doc """ Create an empty Macaroon with a provided `location`, `public_id` and `secret` """ - @spec create_macaroon(binary, binary, binary) :: Types.Macaroon.t() - def create_macaroon(location, public_identifier, secret) + @spec create_macaroon(binary, binary, binary, integer) :: Types.Macaroon.t() + def create_macaroon(location, public_identifier, secret, version \\ 1) when is_binary(location) and is_binary(public_identifier) and is_binary(secret) do derived_key = Util.Crypto.create_derived_key(secret) initial_sig = :crypto.mac(:hmac, :sha256, derived_key, public_identifier) @@ -21,7 +21,8 @@ defmodule Macaroon do Types.Macaroon.build( location: location, public_identifier: public_identifier, - signature: initial_sig + signature: initial_sig, + version: version ) end @@ -148,11 +149,7 @@ defmodule Macaroon do end def serialize(%Types.Macaroon{} = macaroon, :binary) do - Binary.encode(macaroon, :v1) - end - - def serialize_v2(%Types.Macaroon{} = macaroon, :binary) do - Binary.V2.encode(macaroon) + Binary.encode(macaroon) end @doc """ @@ -170,4 +167,8 @@ defmodule Macaroon do def deserialize(macaroon_binary, :binary) when is_binary(macaroon_binary) do Binary.decode(macaroon_binary, :v1) end + + def deserialize_v2(macaroon_binary, :binary) when is_binary(macaroon_binary) do + Binary.decode(macaroon_binary, :v2) + end end diff --git a/lib/serializers/binary.ex b/lib/serializers/binary.ex index 5f8477f..16592da 100644 --- a/lib/serializers/binary.ex +++ b/lib/serializers/binary.ex @@ -10,8 +10,8 @@ defmodule Macaroon.Serializers.Binary do @max_packet_size 65535 - @spec encode(Macaroon.Types.Macaroon.t(), :v1) :: binary | {:error, any} - def encode(%Types.Macaroon{} = macaroon, :v1) do + @spec encode(Macaroon.Types.Macaroon.t()) :: binary | {:error, any} + def encode(%Types.Macaroon{version: 1} = macaroon) do with {:ok, location} <- create_packet_v1("location", macaroon.location), {:ok, id} <- create_packet_v1("identifier", macaroon.public_identifier), {:ok, sig} <- create_packet_v1("signature", macaroon.signature), @@ -26,12 +26,16 @@ defmodule Macaroon.Serializers.Binary do end end - @spec decode(binary, :v1) :: Macaroon.Types.Macaroon.t() + def encode(%Types.Macaroon{version: 2} = macaroon), do: Macaroon.Serializers.Binary.V2.encode(macaroon) + + @spec decode(binary, :v1 | :v2) :: Macaroon.Types.Macaroon.t() def decode(bin_macaroon, :v1) when is_binary(bin_macaroon) do {:ok, decoded} = Base.url_decode64(bin_macaroon, padding: false) do_decode_macaroon_v1(decoded) end + def decode(binary, :v2), do: Macaroon.Serializers.Binary.V2.decode(binary) + # Encoder v1 functions defp encode_caveats_v1(%Types.Macaroon{} = macaroon) do @@ -95,7 +99,10 @@ defmodule Macaroon.Serializers.Binary do packets = do_decode_packets_v1(decoded_bin, []) base_mac = Types.Macaroon.build() mac = do_parse_packets_v1(packets, base_mac) - %Types.Macaroon{mac | caveats: Enum.reverse(mac.caveats)} + + mac + |> Map.put(:caveats, Enum.reverse(mac.caveats)) + |> Map.put(:version, 1) end defp build_third_party_caveat(location, vid, id) do From 9acfacf45acbf036fd30953bd9546937fc06e838 Mon Sep 17 00:00:00 2001 From: jurraca Date: Thu, 3 Aug 2023 23:45:13 +0200 Subject: [PATCH 6/7] handle third-party verification correctly --- lib/macaroon.ex | 2 +- lib/serializers/binary_v2.ex | 34 ++++++++++++++++++++-------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/macaroon.ex b/lib/macaroon.ex index 6a2dd96..b7f7c76 100644 --- a/lib/macaroon.ex +++ b/lib/macaroon.ex @@ -57,7 +57,7 @@ defmodule Macaroon do OR - retreieve an ID from the other service first and use that as the ID. + retrieve an ID from the other service first and use that as the ID. `caveat_key` is the freshly generated secret key that will be encrypted using the current signature of the Macaroon diff --git a/lib/serializers/binary_v2.ex b/lib/serializers/binary_v2.ex index 27a0b2e..7e4e874 100644 --- a/lib/serializers/binary_v2.ex +++ b/lib/serializers/binary_v2.ex @@ -9,6 +9,9 @@ defmodule Macaroon.Serializers.Binary.V2 do @field_types %{location: 1, public_identifier: 2, vid: 4, signature: 6, eos: 0} + @doc """ + Encode a V2 macaroon of type %Macaroon{}. + """ def encode(%Macaroon{ public_identifier: identifier, location: location, @@ -18,6 +21,17 @@ defmodule Macaroon.Serializers.Binary.V2 do encode_macaroon(2, location, identifier, caveats, signature) end + @doc """ + Decode a Base64-encoded macaroon. + """ + def decode(binary) do + {:ok, decoded} = Base.url_decode64(binary, padding: false) + # first we pick off the version + <> = decoded + # then decode the rest + decode_fields(rest, Macaroon.build([version: v])) + end + def encode_macaroon(version, location, identifier, caveats, signature) do with {:ok, location} <- encode_field(@field_types.location, location), {:ok, id} <- encode_field(@field_types.public_identifier, identifier), @@ -91,27 +105,17 @@ defmodule Macaroon.Serializers.Binary.V2 do do: encode_field(@field_types.location, location) defp encode_optional_location(_), do: {:ok, <<>>} - defp encode_optional_vid(%{vid: vid}), do: encode_field(@field_types.vid, vid) + defp encode_optional_vid(%{verification_key_id: vid}) when not is_nil(vid), do: encode_field(@field_types.vid, vid) defp encode_optional_vid(_), do: {:ok, <<>>} - @doc """ - Decode a Base64 encoded macaroon. - """ - def decode_mac(binary) do - {:ok, decoded} = Base.url_decode64(binary, padding: false) - # first we pick off the version - <> = decoded - # then decode the rest - decode_fields(rest, Macaroon.build([version: v])) - end - @doc """ Decode fields of binary macaroon. Fields are recursively accumulated: - if the accumulator is empty, return the decoded macaroon. - - Field Type EOS (a zero byte) delimits the caveats section, and individual caveats within that section. + - field type EOS (a zero byte) delimits the caveats section, and individual caveats within that section. + - if there are no caveats in the decoded macaroon yet, decode them. """ - def decode_fields(<<>>, mac), do: {:ok, mac} + def decode_fields(<<>>, mac), do: mac def decode_fields(<<0::size(8), rest::binary>>, %{caveats: caveats} = mac) do case Enum.count(caveats) > 0 do @@ -187,10 +191,12 @@ defmodule Macaroon.Serializers.Binary.V2 do def add_caveat_section(%{caveats: caveats} = mac, section) do party = if :location in Map.keys(section), do: :third, else: :first + verification_key_id = if party == :third, do: section.vid, else: nil formatted = section |> Map.put(:party, party) |> Map.put(:caveat_id, section.public_identifier) + |> Map.put(:verification_key_id, verification_key_id) |> Enum.into([]) |> Caveat.build() From 442a4f4fa3a91bb6b5f310e0b4d9c394580ae6b2 Mon Sep 17 00:00:00 2001 From: jurraca Date: Thu, 3 Aug 2023 23:45:35 +0200 Subject: [PATCH 7/7] add tests and adapt existing tests to new v1/v2 handling --- test/binary_serializer_test.exs | 52 ++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/test/binary_serializer_test.exs b/test/binary_serializer_test.exs index 63a9ee5..a42d0f0 100644 --- a/test/binary_serializer_test.exs +++ b/test/binary_serializer_test.exs @@ -9,25 +9,43 @@ defmodule BinarySerializerTest do @m_secret "SECRET_CODE" describe "BinarySerializer" do - test "Should seralize an empty macaroon into an encoded string" do + test "Should serialize an empty macaroon into an encoded string" do m = Macaroon.create_macaroon(@m_location, @m_id, @m_secret) {:ok, encoded_string} = Macaroon.serialize(m, :binary) decoded = Macaroon.deserialize(encoded_string, :binary) assert m == decoded end - test "Should seralize a first party caveat" do + test "Should serialize an empty macaroon into a V2 encoded string" do + m = Macaroon.create_macaroon(@m_location, @m_id, @m_secret, 2) + {:ok, encoded_string} = Macaroon.serialize(m, :binary) + decoded = Macaroon.deserialize_v2(encoded_string, :binary) + assert m == decoded + end + + test "Should serialize a first party caveat" do m = Macaroon.create_macaroon(@m_location, @m_id, @m_secret) |> Macaroon.add_first_party_caveat("account = 1234") - {:ok, encoded_string} = Serializers.Binary.encode(m, :v1) + {:ok, encoded_string} = Serializers.Binary.encode(m) decoded = Serializers.Binary.decode(encoded_string, :v1) assert m == decoded end - test "Should seralize a third party caveat" do + test "Should V2 serialize a first party caveat" do + m = + Macaroon.create_macaroon(@m_location, @m_id, @m_secret, 2) + |> Macaroon.add_first_party_caveat("account = 1234") + + {:ok, encoded_string} = Serializers.Binary.encode(m) + + decoded = Serializers.Binary.decode(encoded_string, :v2) + assert m == decoded + end + + test "Should serialize a third party caveat" do static_nonce = Crypto.truncate_or_pad_string(<<0>>, :enacl.secretbox_NONCEBYTES()) m = @@ -39,18 +57,36 @@ defmodule BinarySerializerTest do static_nonce ) - {:ok, encoded_string} = Serializers.Binary.encode(m, :v1) + {:ok, encoded_string} = Serializers.Binary.encode(m) decoded = Serializers.Binary.decode(encoded_string, :v1) assert m == decoded end + test "Should V2 serialize a third party caveat" do + static_nonce = Crypto.truncate_or_pad_string(<<0>>, :enacl.secretbox_NONCEBYTES()) + + m = + Macaroon.create_macaroon(@m_location, @m_id, @m_secret, 2) + |> Macaroon.add_third_party_caveat( + "http://auth.example.com", + "account = 1234", + "SECRET_KEY_TP", + static_nonce + ) + + {:ok, encoded_string} = Serializers.Binary.encode(m) + + decoded = Serializers.Binary.decode(encoded_string, :v2) + assert m == decoded + end + test "Should fail to serialize a first party packet that is too long" do m = Macaroon.create_macaroon(@m_location, @m_id, @m_secret) |> Macaroon.add_first_party_caveat(String.pad_leading("a = ", 70000, "b")) - assert {:error, _} = Serializers.Binary.encode(m, :v1) + assert {:error, _} = Serializers.Binary.encode(m) end test "Should fail to serialize a third party packet that is too long" do @@ -58,7 +94,7 @@ defmodule BinarySerializerTest do Macaroon.create_macaroon(@m_location, @m_id, @m_secret) |> Macaroon.add_third_party_caveat(String.pad_leading("a = ", 70000, "b"), "id", "key") - assert {:error, _} = Serializers.Binary.encode(m, :v1) + assert {:error, _} = Serializers.Binary.encode(m) end test "should not trim off valid signature bytes" do @@ -72,7 +108,7 @@ defmodule BinarySerializerTest do 206, 200, 244, 115, 124, 229, 79, 38, 200, 44, 168, 9, 163, 12>> } - {:ok, encoded_string} = Serializers.Binary.encode(m, :v1) + {:ok, encoded_string} = Serializers.Binary.encode(m) decoded = Serializers.Binary.decode(encoded_string, :v1) assert m == decoded