summaryrefslogtreecommitdiff
path: root/macaroonbakery/macaroon.py
diff options
context:
space:
mode:
Diffstat (limited to 'macaroonbakery/macaroon.py')
-rw-r--r--macaroonbakery/macaroon.py440
1 files changed, 265 insertions, 175 deletions
diff --git a/macaroonbakery/macaroon.py b/macaroonbakery/macaroon.py
index 954161c..6f6039e 100644
--- a/macaroonbakery/macaroon.py
+++ b/macaroonbakery/macaroon.py
@@ -1,37 +1,30 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-
+import abc
import base64
-import copy
+import json
import logging
import os
-from macaroonbakery import bakery
-from macaroonbakery import codec
import pymacaroons
+from pymacaroons.serializers import json_serializer
-from macaroonbakery import namespace
+import macaroonbakery
+import macaroonbakery.checkers as checkers
+from macaroonbakery import utils
-MACAROON_V1, MACAROON_V2 = 1, 2
log = logging.getLogger(__name__)
-def legacy_namespace():
- ''' Standard namespace for pre-version3 macaroons.
- '''
- ns = namespace.Namespace(None)
- ns.register(namespace.STD_NAMESPACE, '')
- return ns
-
-
-class Macaroon:
- '''Represent an undischarged macaroon along its first
+class Macaroon(object):
+ '''Represent an undischarged macaroon along with its first
party caveat namespace and associated third party caveat information
which should be passed to the third party when discharging a caveat.
'''
+
def __init__(self, root_key, id, location=None,
- version=bakery.LATEST_BAKERY_VERSION, ns=None):
+ version=macaroonbakery.LATEST_BAKERY_VERSION, namespace=None):
'''Creates a new macaroon with the given root key, id and location.
If the version is more than the latest known version,
@@ -41,22 +34,45 @@ class Macaroon:
@param id bytes or string
@param location bytes or string
@param version the bakery version.
- @param ns
+ @param namespace is that of the service creating it
'''
- if version > bakery.LATEST_BAKERY_VERSION:
+ if version > macaroonbakery.LATEST_BAKERY_VERSION:
log.info('use last known version:{} instead of: {}'.format(
- bakery.LATEST_BAKERY_VERSION, version
+ macaroonbakery.LATEST_BAKERY_VERSION, version
))
- version = bakery.LATEST_BAKERY_VERSION
+ version = macaroonbakery.LATEST_BAKERY_VERSION
# m holds the underlying macaroon.
- self._macaroon = pymacaroons.Macaroon(location=location, key=root_key,
- identifier=id)
+ self._macaroon = pymacaroons.Macaroon(
+ location=location, key=root_key, identifier=id,
+ version=macaroon_version(version))
# version holds the version of the macaroon.
- self.version = macaroon_version(version)
- self.caveat_data = {}
+ self._version = version
+ self._caveat_data = {}
+ if namespace is None:
+ namespace = checkers.Namespace()
+ self._namespace = namespace
+ self._caveat_id_prefix = bytearray()
+
+ @property
+ def macaroon(self):
+ ''' Return the underlying macaroon.
+ '''
+ return self._macaroon
+
+ @property
+ def version(self):
+ return self._version
+
+ @property
+ def namespace(self):
+ return self._namespace
+
+ @property
+ def caveat_data(self):
+ return self._caveat_data
def add_caveat(self, cav, key=None, loc=None):
- '''Return a new macaroon with the given caveat added.
+ '''Add a caveat to the macaroon.
It encrypts it using the given key pair
and by looking up the location using the given locator.
@@ -68,96 +84,138 @@ class Macaroon:
key.
@param cav the checkers.Caveat to be added.
- @param key the nacl public key to encrypt third party caveat.
+ @param key the public key to encrypt third party caveat.
@param loc locator to find information on third parties when adding
third party caveats. It is expected to have a third_party_info method
that will be called with a location string and should return a
ThirdPartyInfo instance holding the requested information.
- @return a new macaroon object with the given caveat.
'''
if cav.location is None:
- macaroon = self._macaroon.add_first_party_caveat(cav.condition)
- new_macaroon = copy.copy(self)
- new_macaroon._macaroon = macaroon
- return new_macaroon
+ self._macaroon.add_first_party_caveat(
+ self.namespace.resolve_caveat(cav).condition)
+ return
if key is None:
raise ValueError(
'no private key to encrypt third party caveat')
- local_info, ok = parse_local_location(cav.location)
- if ok:
+ local_info = _parse_local_location(cav.location)
+ if local_info is not None:
info = local_info
- cav.location = 'local'
if cav.condition is not '':
raise ValueError(
'cannot specify caveat condition in '
'local third-party caveat')
- cav.condition = 'true'
+ cav = checkers.Caveat(location='local', condition='true')
else:
if loc is None:
raise ValueError(
'no locator when adding third party caveat')
info = loc.third_party_info(cav.location)
+
root_key = os.urandom(24)
+
# Use the least supported version to encode the caveat.
- if self.version < info.version:
- info.version = self.version
+ if self._version < info.version:
+ info = macaroonbakery.ThirdPartyInfo(version=self._version,
+ public_key=info.public_key)
- caveat_info = codec.encode_caveat(cav.condition, root_key, info,
- key, None)
- if info.version < bakery.BAKERY_V3:
+ caveat_info = macaroonbakery.encode_caveat(
+ cav.condition, root_key, info, key, self._namespace)
+ if info.version < macaroonbakery.BAKERY_V3:
# We're encoding for an earlier client or third party which does
# not understand bundled caveat info, so use the encoded
# caveat information as the caveat id.
id = caveat_info
else:
- id = self._new_caveat_id(self.caveat_id_prefix)
- self.caveat_data[id] = caveat_info
+ id = self._new_caveat_id(self._caveat_id_prefix)
+ self._caveat_data[id] = caveat_info
- m = self._macaroon.add_third_party_caveat(cav.location, root_key, id)
- new_macaroon = copy.copy(self)
- new_macaroon._macaroon = m
- return new_macaroon
+ self._macaroon.add_third_party_caveat(cav.location, root_key, id)
def add_caveats(self, cavs, key, loc):
- '''Return a new macaroon with all caveats added.
+ '''Add an array of caveats to the macaroon.
This method does not mutate the current object.
@param cavs arrary of caveats.
- @param key the nacl public key to encrypt third party caveat.
+ @param key the PublicKey to encrypt third party caveat.
@param loc locator to find the location object that has a method
third_party_info.
- @return a new macaroon object with the given caveats.
'''
- macaroon = self
+ if cavs is None:
+ return
for cav in cavs:
- macaroon = macaroon.add_caveat(cav, key, loc)
- return macaroon
+ self.add_caveat(cav, key, loc)
- def serialize(self):
- '''Return a dictionary holding the macaroon data in V1 JSON format.
-
- Note that this differs from the underlying macaroon serialize method as
- it does not return a string. This makes it easier to incorporate the
- macaroon into other JSON objects.
+ def serialize_json(self):
+ '''Return a string holding the macaroon data in JSON format.
+ @return a string holding the macaroon data in JSON format
+ '''
+ return json.dumps(self.to_dict())
- @return a dictionary holding the macaroon data
- in V1 JSON format
+ def to_dict(self):
+ '''Return a dict representation of the macaroon data in JSON format.
+ @return a dict
'''
- if self.version == bakery.BAKERY_V1:
- # latest libmacaroons do not support the old format
- json_macaroon = self._macaroon.serialize('json')
- val = {
- 'identifier': _field_v2(json_macaroon, 'i'),
- 'signature': _field_v2(json_macaroon, 's'),
- }
- location = json_macaroon.get('l')
- if location is not None:
- val['location'] = location
- cavs = json_macaroon.get('c')
- if cavs is not None:
- val['caveats'] = map(cavs, _cav_v2_to_v1)
- return val
- raise NotImplementedError('only bakery v1 supported')
+ if self.version < macaroonbakery.BAKERY_V3:
+ if len(self._caveat_data) > 0:
+ raise ValueError('cannot serialize pre-version3 macaroon with '
+ 'external caveat data')
+ return json.loads(self._macaroon.serialize(
+ json_serializer.JsonSerializer()))
+ serialized = {
+ 'm': json.loads(self._macaroon.serialize(
+ json_serializer.JsonSerializer())),
+ 'v': self._version,
+ }
+ if self._namespace is not None:
+ serialized['ns'] = self._namespace.serialize_text().decode('utf-8')
+ caveat_data = {}
+ for id in self._caveat_data:
+ key = base64.b64encode(id).decode('utf-8')
+ value = base64.b64encode(self._caveat_data[id]).decode('utf-8')
+ caveat_data[key] = value
+ if len(caveat_data) > 0:
+ serialized['cdata'] = caveat_data
+ return serialized
+
+ @classmethod
+ def deserialize_json(cls, serialized_json):
+ serialized = json.loads(serialized_json)
+ json_macaroon = serialized.get('m')
+ if json_macaroon is None:
+ # Try the v1 format if we don't have a macaroon filed
+ m = pymacaroons.Macaroon.deserialize(
+ serialized_json, json_serializer.JsonSerializer())
+ macaroon = Macaroon(root_key=None, id=None,
+ namespace=macaroonbakery.legacy_namespace(),
+ version=_bakery_version(m.version))
+ macaroon._macaroon = m
+ return macaroon
+
+ version = serialized.get('v', None)
+ if version is None:
+ raise ValueError('no version specified')
+ if (version < macaroonbakery.BAKERY_V3 or
+ version > macaroonbakery.LATEST_BAKERY_VERSION):
+ raise ValueError('unknow bakery version {}'.format(version))
+ m = pymacaroons.Macaroon.deserialize(json.dumps(json_macaroon),
+ json_serializer.JsonSerializer())
+ if m.version != macaroon_version(version):
+ raise ValueError(
+ 'underlying macaroon has inconsistent version; '
+ 'got {} want {}'.format(m.version, macaroon_version(version)))
+ namespace = checkers.deserialize_namespace(serialized.get('ns'))
+ cdata = serialized.get('cdata', {})
+ caveat_data = {}
+ for id64 in cdata:
+ id = utils.raw_b64decode(id64)
+ data = utils.raw_b64decode(cdata[id64])
+ caveat_data[id] = data
+ macaroon = Macaroon(root_key=None, id=None,
+ namespace=namespace,
+ version=version)
+ macaroon._caveat_data = caveat_data
+ macaroon._macaroon = m
+ return macaroon
def _new_caveat_id(self, base):
'''Return a third party caveat id
@@ -165,10 +223,44 @@ class Macaroon:
This does not duplicate any third party caveat ids already inside
macaroon. If base is non-empty, it is used as the id prefix.
- @param base string
- @return string
+ @param base bytes
+ @return bytes
'''
- raise NotImplementedError
+ id = bytearray()
+ if len(base) > 0:
+ id.extend(base)
+ else:
+ # Add a version byte to the caveat id. Technically
+ # this is unnecessary as the caveat-decoding logic
+ # that looks at versions should never see this id,
+ # but if the caveat payload isn't provided with the
+ # payload, having this version gives a strong indication
+ # that the payload has been omitted so we can produce
+ # a better error for the user.
+ id.append(macaroonbakery.BAKERY_V3)
+
+ # Iterate through integers looking for one that isn't already used,
+ # starting from n so that if everyone is using this same algorithm,
+ # we'll only perform one iteration.
+ i = len(self._caveat_data)
+ caveats = self._macaroon.caveats
+ while True:
+ # We append a varint to the end of the id and assume that
+ # any client that's created the id that we're using as a base
+ # is using similar conventions - in the worst case they might
+ # end up with a duplicate third party caveat id and thus create
+ # a macaroon that cannot be discharged.
+ temp = id[:]
+ macaroonbakery.encode_uvarint(i, temp)
+ found = False
+ for cav in caveats:
+ if (cav.verification_key_id is not None
+ and cav.caveat_id == temp):
+ found = True
+ break
+ if not found:
+ return bytes(temp)
+ i += 1
def first_party_caveats(self):
'''Return the first party caveats from this macaroon.
@@ -185,6 +277,18 @@ class Macaroon:
'''
return self._macaroon.third_party_caveats()
+ def copy(self):
+ ''' Returns a copy of the macaroon. Note that the the new
+ macaroon's namespace still points to the same underlying Namespace -
+ copying the macaroon does not make a copy of the namespace.
+ :return a Macaroon
+ '''
+ m1 = Macaroon(None, None, version=self._version,
+ namespace=self._namespace)
+ m1._macaroon = self._macaroon.copy()
+ m1._caveat_data = self._caveat_data.copy()
+ return m1
+
def macaroon_version(bakery_version):
'''Return the macaroon version given the bakery version.
@@ -192,12 +296,50 @@ def macaroon_version(bakery_version):
@param bakery_version the bakery version
@return macaroon_version the derived macaroon version
'''
- if bakery_version in [bakery.BAKERY_V0, bakery.BAKERY_V1]:
- return MACAROON_V1
- return MACAROON_V2
+ if bakery_version in [macaroonbakery.BAKERY_V0, macaroonbakery.BAKERY_V1]:
+ return pymacaroons.MACAROON_V1
+ return pymacaroons.MACAROON_V2
+
+
+class ThirdPartyLocator(object):
+ '''Used to find information on third party discharge services.
+ '''
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def third_party_info(self, loc):
+ '''Return information on the third party at the given location.
+ @param loc string
+ @return: a ThirdPartyInfo
+ @raise: ThirdPartyInfoNotFound
+ '''
+ raise NotImplementedError('third_party_info method must be defined in '
+ 'subclass')
+
+
+class ThirdPartyStore(ThirdPartyLocator):
+ ''' Implements a simple in memory ThirdPartyLocator.
+ '''
+ def __init__(self):
+ self._store = {}
+
+ def third_party_info(self, loc):
+ info = self._store.get(loc.rstrip('/'))
+ if info is None:
+ raise macaroonbakery.ThirdPartyInfoNotFound(
+ 'cannot retrieve the info for location {}'.format(loc))
+ return info
+
+ def add_info(self, loc, info):
+ '''Associates the given information with the given location.
+ It will ignore any trailing slash.
+ @param loc the location as string
+ @param info (ThirdPartyInfo) to store for this location.
+ '''
+ self._store[loc.rstrip('/')] = info
-def parse_local_location(loc):
+def _parse_local_location(loc):
'''Parse a local caveat location as generated by LocalThirdPartyCaveat.
This is of the form:
@@ -207,105 +349,53 @@ def parse_local_location(loc):
where <version> is the bakery version of the client that we're
adding the local caveat for.
- It returns false if the location does not represent a local
+ It returns None if the location does not represent a local
caveat location.
- @return a tuple of location and if the location is local.
+ @return a ThirdPartyInfo.
'''
- if not(loc.startswith('local ')):
- return (), False
- v = bakery.BAKERY_V1
+ if not (loc.startswith('local ')):
+ return None
+ v = macaroonbakery.BAKERY_V1
fields = loc.split()
fields = fields[1:] # Skip 'local'
if len(fields) == 2:
try:
v = int(fields[0])
except ValueError:
- return (), False
+ return None
fields = fields[1:]
if len(fields) == 1:
- return (base64.b64decode(fields[0]), v), True
- return (), False
-
-
-class ThirdPartyLocator:
- '''Used to find information on third party discharge services.
- '''
- def __init__(self):
- self._store = {}
-
- def third_party_info(self, loc):
- '''Return information on the third party at the given location.
-
- It returns None if no match is found.
-
- @param loc string
- @return: string
- '''
- return self._store.get(loc)
-
- def add_info(self, loc, info):
- '''Associates the given information with the given location.
-
- It will ignore any trailing slash.
- '''
- self._store[loc.rstrip('\\')] = info
-
-
-class ThirdPartyCaveatInfo:
- '''ThirdPartyCaveatInfo holds the information decoded from
- a third party caveat id.
- '''
- def __init__(self, condition, first_party_public_key, third_party_key_pair,
- root_key, caveat, version, ns):
- '''
- @param condition holds the third party condition to be discharged.
- This is the only field that most third party dischargers will
- need to consider.
- @param first_party_public_key holds the nacl public key of the party
- that created the third party caveat.
- @param third_party_key_pair holds the nacl private used to decrypt
- the caveat - the key pair of the discharging service.
- @param root_key bytes holds the secret root key encoded by the caveat.
- @param caveat holds the full encoded base64 string caveat id from
- which all the other fields are derived.
- @param version holds the version that was used to encode
- the caveat id.
- @params Namespace object that holds the namespace of the first party
- that created the macaroon, as encoded by the party that added the
- third party caveat.
- '''
- self.condition = condition,
- self.first_party_public_key = first_party_public_key,
- self.third_party_key_pair = third_party_key_pair,
- self.root_key = root_key,
- self.caveat = caveat,
- self.version = version,
- self.ns = ns
-
- def __eq__(self, other):
- return (
- self.condition == other.condition and
- self.first_party_public_key == other.first_party_public_key and
- self.third_party_key_pair == other.third_party_key_pair and
- self.caveat == other.caveat and
- self.version == other.version and
- self.ns == other.ns
- )
-
-
-def _field_v2(dict, field):
- val = dict.get(field)
- if val is None:
- return base64.b64decode(dict.get(field + '64'))
- return val
-
-
-def _cav_v2_to_v1(cav):
- val = {
- 'cid': _field_v2(cav, 'i'),
- 'vid': _field_v2(cav, 'v')
- }
- location = cav.get('l')
- if location is not None:
- val['cl'] = location
- return val
+ key = macaroonbakery.PublicKey.deserialize(fields[0])
+ return macaroonbakery.ThirdPartyInfo(public_key=key,
+ version=v)
+ return None
+
+
+def _bakery_version(v):
+ # bakery_version returns a bakery version that corresponds to
+ # the macaroon version v. It is necessarily approximate because
+ # several bakery versions can correspond to a single macaroon
+ # version, so it's only of use when decoding legacy formats
+ #
+ # It will raise a ValueError if it doesn't recognize the version.
+ if v == pymacaroons.MACAROON_V1:
+ # Use version 1 because we don't know of any existing
+ # version 0 clients.
+ return macaroonbakery.BAKERY_V1
+ elif v == pymacaroons.MACAROON_V2:
+ # Note that this could also correspond to Version 3, but
+ # this logic is explicitly for legacy versions.
+ return macaroonbakery.BAKERY_V2
+ else:
+ raise ValueError('unknown macaroon version when deserializing legacy '
+ 'bakery macaroon; got {}'.format(v))
+
+
+class MacaroonJSONEncoder(json.JSONEncoder):
+ def encode(self, m):
+ return m.serialize_json()
+
+
+class MacaroonJSONDecoder(json.JSONDecoder):
+ def decode(self, s, _w=json.decoder.WHITESPACE.match):
+ return Macaroon.deserialize_json(s)