summaryrefslogtreecommitdiff
path: root/macaroonbakery/macaroon.py
blob: b0a89bb9a3469e94532dc72390cfba5adbb59b15 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.

import base64
import copy
import logging
import os

import bakery
import codec
import pymacaroons

import namespace

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
    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):
        '''Creates a new macaroon with the given root key, id and location.

        If the version is more than the latest known version,
        the latest known version will be used. The namespace should hold the
        namespace of the service that is creating the macaroon.
        @param root_key bytes or string
        @param id bytes or string
        @param location bytes or string
        @param version the bakery version.
        @param ns
        '''
        if version > bakery.LATEST_BAKERY_VERSION:
            log.info('use last known version:{} instead of: {}'.format(
                bakery.LATEST_BAKERY_VERSION, version
            ))
            version = bakery.LATEST_BAKERY_VERSION
        # m holds the underlying macaroon.
        self._macaroon = pymacaroons.Macaroon(location=location, key=root_key,
                                              identifier=id)
        # version holds the version of the macaroon.
        self.version = macaroon_version(version)
        self.caveat_data = {}

    def add_caveat(self, cav, key=None, loc=None):
        '''Return a new macaroon with the given caveat added.

        It encrypts it using the given key pair
        and by looking up the location using the given locator.
        As a special case, if the caveat's Location field has the prefix
        "local " the caveat is added as a client self-discharge caveat using
        the public key base64-encoded in the rest of the location. In this
        case, the Condition field must be empty. The resulting third-party
        caveat will encode the condition "true" encrypted with that public
        key.

        @param cav the checkers.Caveat to be added.
        @param key the nacl 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
        if key is None:
            raise ValueError(
                'no private key to encrypt third party caveat')
        local_info, ok = parse_local_location(cav.location)
        if ok:
            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'
        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

        caveat_info = codec.encode_caveat(cav.condition, root_key, info,
                                          key, None)
        if info.version < bakery.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

        m = self._macaroon.add_third_party_caveat(cav.location, root_key, id)
        new_macaroon = copy.copy(self)
        new_macaroon._macaroon = m
        return new_macaroon

    def add_caveats(self, cavs, key, loc):
        '''Return a new macaroon with all caveats added.

        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 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
        for cav in cavs:
            macaroon = macaroon.add_caveat(cav, key, loc)
        return macaroon

    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.

        @return a dictionary holding the macaroon data
        in V1 JSON format
        '''
        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')

    def _new_caveat_id(self, base):
        '''Return a third party caveat id

        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
        '''
        raise NotImplementedError

    def first_party_caveats(self):
        '''Return the first party caveats from this macaroon.

        @return the first party caveats from this macaroon as pymacaroons
        caveats.
        '''
        return self._macaroon.first_party_caveats()

    def third_party_caveats(self):
        '''Return the third party caveats.

        @return the third party caveats as pymacaroons caveats.
        '''
        return self._macaroon.third_party_caveats()


def macaroon_version(bakery_version):
    '''Return the macaroon version given the 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


def parse_local_location(loc):
    '''Parse a local caveat location as generated by LocalThirdPartyCaveat.

    This is of the form:

        local <version> <pubkey>

    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
    caveat location.
    @return a tuple of location and if the location is local.
    '''
    if not(loc.startswith('local ')):
        return (), False
    v = bakery.BAKERY_V1
    fields = loc.split()
    fields = fields[1:]  # Skip 'local'
    if len(fields) == 2:
        try:
            v = int(fields[0])
        except ValueError:
            return (), False
        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