blob: 3e288b17bdc4a4756a0a93e9569d76daaa4cdd70 [file] [log] [blame]
Mate Toth-Palbb187d02022-04-26 16:01:51 +02001# -----------------------------------------------------------------------------
2# Copyright (c) 2019-2022, Arm Limited. All rights reserved.
3#
4# SPDX-License-Identifier: BSD-3-Clause
5#
6# -----------------------------------------------------------------------------
7
Mate Toth-Palb9057ff2022-04-29 16:03:21 +02008"""
9Class definitions to use as base for claim and verifier classes.
10"""
11
12
Mate Toth-Palbb187d02022-04-26 16:01:51 +020013import logging
Mate Toth-Pald10a9142022-04-28 15:34:13 +020014from abc import ABC, abstractmethod
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020015from dataclasses import dataclass
16from io import BytesIO
17
18from pycose.attributes import CoseAttrs
19from pycose.sign1message import Sign1Message
20from pycose.mac0message import Mac0Message
Mate Toth-Palbb187d02022-04-26 16:01:51 +020021
22import cbor2
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020023from cbor2 import CBOREncoder
Mate Toth-Pale589c452022-07-27 22:02:40 +020024import _cbor2
Mate Toth-Palbb187d02022-04-26 16:01:51 +020025
26logger = logging.getLogger('iat-verifiers')
27
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020028_CBOR_MAJOR_TYPE_ARRAY = 4
29_CBOR_MAJOR_TYPE_MAP = 5
30_CBOR_MAJOR_TYPE_SEMANTIC_TAG = 6
31
Mate Toth-Palc7404e92022-07-15 11:11:13 +020032class TokenItem:
33 """This class represents an item in the token map
34
35 The Field `claim_type` contains an AttestationClaim object, that determines how to interpret the
36 `value` field.
37 The field `value` contains either another TokenItem object or a representation of a claim value
38 (list, dictionary, bytestring...) depending on the value of `claim_type`
39
40 A TokenItem object might have extra fields beyond these as it might be necessary to store
41 properties during parsing, that can aid verifying.
42 """
43 def __init__(self, *, value, claim_type):
44 self.value = value # The value of the claim
45 self.claim_type = claim_type # an AttestationClaim instance
46
47 @classmethod
48 def _call_verify_with_parents(cls, claim_type_class, claim_type, token_item, indent):
49 for parent_class in claim_type_class.__bases__:
50 cls._call_verify_with_parents(parent_class, claim_type, token_item, indent + 2)
51 if "verify" in claim_type_class.__dict__:
52 claim_type_class.verify(claim_type, token_item)
53
54 def verify(self):
55 """Calls claim_type's and its parents' verify method"""
56 claim_type = self.claim_type
57 self.__class__._call_verify_with_parents(claim_type.__class__, claim_type, self, 0)
58
59 def get_token_map(self):
60 return self.claim_type.get_token_map(self)
61
Mate Toth-Pald10a9142022-04-28 15:34:13 +020062class AttestationClaim(ABC):
63 """
64 This class represents a claim.
65
66 This class is abstract. A concrete claim have to be derived from this class,
67 and it have to implement all the abstract methods.
68
69 This class contains methods that are not abstract. These are here as a
70 default behavior, that a derived class might either keep, or override.
Mate Toth-Pald10a9142022-04-28 15:34:13 +020071 """
72
Mate Toth-Palbb187d02022-04-26 16:01:51 +020073 MANDATORY = 0
74 RECOMMENDED = 1
75 OPTIONAL = 2
76
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020077 def __init__(self, *, verifier, necessity=MANDATORY):
Mate Toth-Palbb187d02022-04-26 16:01:51 +020078 self.config = verifier.config
79 self.verifier = verifier
80 self.necessity = necessity
Mate Toth-Palbb187d02022-04-26 16:01:51 +020081
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020082 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +020083 # Abstract methods
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020084 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +020085
86 @abstractmethod
Mate Toth-Palbb187d02022-04-26 16:01:51 +020087 def get_claim_key(self=None):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020088 """Get the key of this claim
89
90 Returns the key of this claim. The implementation have to support
91 calling this method with or without an instance as well."""
Mate Toth-Palbb187d02022-04-26 16:01:51 +020092 raise NotImplementedError
93
Mate Toth-Pald10a9142022-04-28 15:34:13 +020094 @abstractmethod
Mate Toth-Palbb187d02022-04-26 16:01:51 +020095 def get_claim_name(self=None):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020096 """Get the name of this claim
97
98 Returns the name of this claim. The implementation have to support
99 calling this method with or without an instance as well."""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200100 raise NotImplementedError
101
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200102 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200103 # Default methods that a derived class might override
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200104 #
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200105
106 def decode(self, value):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200107 """
108 Decode the value of the claim if the value is an UTF-8 string
109 """
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200110 if self.__class__.is_utf_8():
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200111 try:
112 return value.decode()
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200113 except UnicodeDecodeError as exc:
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200114 msg = 'Error decodeing value for "{}": {}'
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200115 self.verifier.error(msg.format(self.get_claim_name(), exc))
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200116 return str(value)[2:-1]
117 else: # not a UTF-8 value, i.e. a bytestring
118 return value
119
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200120 @classmethod
121 def is_utf_8(cls):
122 """Returns whether the value of this claim should be UTF-8"""
123 return False
124
125 def convert_map_to_token(self,
126 token_encoder,
127 token_map,
128 *, add_p_header,
129 name_as_key,
130 parse_raw_value):
131 """Encode a map in cbor format using the 'token_encoder'"""
132 # pylint: disable=unused-argument
133 value = token_map
134 if parse_raw_value:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200135 value = self.__class__.parse_raw(value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200136 return token_encoder.encode(value)
137
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200138 def parse_token(self, *, token, check_p_header, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200139 """Parse a token into a map
140
141 This function is recursive for composite claims and for token verifiers.
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200142
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200143 The `token` parameter can be interpreted differently in derived classes:
144 - as a raw token that is decoded by the CBOR parsing library
145 - as CBOR encoded token in case of (nested) tokens.
146 """
147 return TokenItem(value=token, claim_type=self)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200148
149 @classmethod
150 def parse_raw(cls, raw_value):
151 """Parse a raw value
152
153 Takes a string, as it appears in a yaml file, and converts it to a
154 numeric value according to the claim's definition.
155 """
156 return raw_value
157
158 @classmethod
159 def get_formatted_value(cls, value):
160 """Format the value according to this claim"""
161 if cls.is_utf_8():
162 # this is an UTF-8 value, force string type
163 return f'{value}'
164 return value
165
166 #
167 # Helper functions to be called from derived classes
168 #
169
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200170 def _check_type(self, name, value, expected_type):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200171 """Check that a value's type is as expected"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200172 if not isinstance(value, expected_type):
173 msg = 'Invalid {}: must be a(n) {}: found {}'
174 self.verifier.error(msg.format(name, expected_type, type(value)))
175 return False
176 return True
177
178 def _validate_bytestring_length_equals(self, value, name, expected_len):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200179 """Check that a bytestring length is as expected"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200180 self._check_type(name, value, bytes)
181
182 value_len = len(value)
183 if value_len != expected_len:
184 msg = 'Invalid {} length: must be exactly {} bytes, found {} bytes'
185 self.verifier.error(msg.format(name, expected_len, value_len))
186
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200187 def _validate_bytestring_length_one_of(self, value, name, possible_lens):
188 """Check that a bytestring length is as expected"""
189 self._check_type(name, value, bytes)
190
191 value_len = len(value)
192 if value_len not in possible_lens:
193 msg = 'Invalid {} length: must be one of {} bytes, found {} bytes'
194 self.verifier.error(msg.format(name, possible_lens, value_len))
195
196 def _validate_bytestring_length_between(self, value, name, min_len, max_len):
197 """Check that a bytestring length is as expected"""
198 self._check_type(name, value, bytes)
199
200 value_len = len(value)
201 if value_len < min_len or value_len > max_len:
202 msg = 'Invalid {} length: must be between {} and {} bytes, found {} bytes'
203 self.verifier.error(msg.format(name, min_len, max_len, value_len))
204
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200205 def _validate_bytestring_length_is_at_least(self, value, name, minimal_length):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200206 """Check that a bytestring has a minimum length"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200207 self._check_type(name, value, bytes)
208
209 value_len = len(value)
210 if value_len < minimal_length:
211 msg = 'Invalid {} length: must be at least {} bytes, found {} bytes'
212 self.verifier.error(msg.format(name, minimal_length, value_len))
213
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200214 def get_token_map(self, token_item):
215 formatted = self.__class__.get_formatted_value(token_item.value)
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200216
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200217 # If the formatted value is still a bytestring then try to decode
218 if isinstance(formatted, bytes):
219 formatted = self.decode(formatted)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200220
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200221 return formatted
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200222
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200223class CompositeAttestClaim(AttestationClaim):
224 """
225 This class represents composite claim.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200226
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200227 This class is still abstract, but can contain other claims. This means that
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200228 a value representing this claim is a dictionary, or a list of dictionaries.
229 This claim contains further claims which represent the possible key-value
230 pairs in the value for this claim.
Mate Toth-Pal530106f2022-05-03 15:29:49 +0200231
232 It is possible that there are requirement that the claims in this claim must
233 satisfy, but this can't be checked in the `verify` function of a claim.
234
235 For example the composite claim can contain a claim type `A`, and a claim
236 type `B`, exactly one of the two can be present.
237
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200238 In this case the class inheriting from this class can have its own verify()
239 method.
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200240 """
241
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200242 def __init__(self,
243 *, verifier,
244 claims,
245 is_list,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200246 necessity=AttestationClaim.MANDATORY):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200247 """ Initialise a composite claim.
248
249 In case 'is_list' is False, the expected type of value is a dictionary,
250 containing the necessary claims determined by the 'claims' list.
251 In case 'is_list' is True, the expected type of value is a list,
252 containing a number of dictionaries, each one containing the necessary
253 claims determined by the 'claims' list.
254 """
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200255 super().__init__(verifier=verifier, necessity=necessity)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200256 self.is_list = is_list
257 self.claims = claims
258
259 def _get_contained_claims(self):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200260 for claim, args in self.claims:
261 try:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200262 yield claim(**args)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200263 except TypeError as exc:
264 raise TypeError(f"Failed to instantiate '{claim}' with args '{args}' in token " +
265 f"{type(self.verifier)}\nSee error in exception above.") from exc
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200266
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200267
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200268 def _verify_dict(self, claim_type, entry_number, dictionary):
269 if not isinstance(dictionary, dict):
270 if self.config.strict:
271 msg = 'The values in token {} must be a dict.'
272 self.verifier.error(msg.format(claim_type.get_claim_name()))
273 else:
274 msg = 'The values in token {} must be a dict, skipping'
275 self.verifier.warning(msg.format(claim_type.get_claim_name()))
276 return
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200277
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200278 claim_names = [val.get_claim_name() for val in claim_type._get_contained_claims()]
279 for claim_name, _ in dictionary.items():
280 if claim_name not in claim_names:
281 if self.config.strict:
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200282 msg = 'Unexpected {} claim: {}'
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200283 self.verifier.error(msg.format(claim_type.get_claim_name(), claim_name))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200284 else:
Mate Toth-Pal5ebca512022-03-24 16:45:51 +0100285 msg = 'Unexpected {} claim: {}, skipping.'
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200286 self.verifier.warning(msg.format(claim_type.get_claim_name(), claim_name))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200287 continue
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200288
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200289 claims = {val.get_claim_key(): val for val in claim_type._get_contained_claims()}
290 self._check_claims_necessity(entry_number, claims, dictionary)
291 for token_item in dictionary.values():
292 if isinstance(token_item, TokenItem):
293 token_item.verify()
294 else:
295 # the parse of this token item failed. So it cannot be verified.
296 # Warning had been reported during parsing.
297 pass
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200298
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200299 def verify(self, token_item):
300 if self.is_list:
301 if not isinstance(token_item.value, list):
302 if self.config.strict:
303 msg = 'The value of this token {} must be a list.'
304 self.verifier.error(msg.format(self.get_claim_name()))
305 else:
306 msg = 'The value of this token {} must be a list, skipping'
307 self.verifier.warning(msg.format(self.get_claim_name()))
308 return
309 for entry_number, list_item in enumerate(token_item.value):
310 self._verify_dict(token_item.claim_type, entry_number, list_item)
311 else:
312 self._verify_dict(token_item.claim_type, None, token_item.value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200313
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200314 def _parse_token_dict(self, *, entry_number, token, check_p_header, lower_case_key):
315 claim_value = {}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200316
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200317 if not isinstance(token, dict):
318 claim_value = token
319 else:
320 claims = {val.get_claim_key(): val for val in self._get_contained_claims()}
321 for key, val in token.items():
322 try:
323 claim = claims[key]
324 name = claim.get_claim_name()
325 if lower_case_key:
326 name = name.lower()
327 claim_value[name] = claim.parse_token(
328 token=val,
329 check_p_header=check_p_header,
330 lower_case_key=lower_case_key)
331 except KeyError:
332 claim_value[key] = val
333 except Exception:
334 if not self.config.keep_going:
335 raise
336 return claim_value
337
338 def _check_claims_necessity(self, entry_number, claims, dictionary):
339 mandatory_claim_names = [claim.get_claim_name() for claim in claims.values() if claim.necessity == AttestationClaim.MANDATORY]
340 recommended_claim_names = [claim.get_claim_name() for claim in claims.values() if claim.necessity == AttestationClaim.RECOMMENDED]
341 dictionary_claim_names = dictionary.keys()
342
343 for mandatory_claim_name in mandatory_claim_names:
344 if mandatory_claim_name not in dictionary_claim_names:
345 msg = (f'Invalid IAT: missing MANDATORY claim "{mandatory_claim_name}" '
346 f'from {self.get_claim_name()}')
347 if entry_number is not None:
348 msg += f' at index {entry_number}'
349 self.verifier.error(msg)
350
351 for recommended_claim_name in recommended_claim_names:
352 if recommended_claim_name not in dictionary_claim_names:
353 msg = (f'Missing RECOMMENDED claim "{recommended_claim_name}" '
354 f'from {self.get_claim_name()}')
355 if entry_number is not None:
356 msg += f' at index {entry_number}'
357 self.verifier.warning(msg)
358
359 def parse_token(self, *, token, check_p_header, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200360 """This expects a raw token map as 'token'"""
361
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200362 if self.is_list:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200363 claim_value = []
364 if not isinstance(token, list):
365 claim_value = token
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200366 else:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200367 for entry_number, entry in enumerate(token):
368 claim_value.append(self._parse_token_dict(
369 entry_number=entry_number,
370 check_p_header=check_p_header,
371 token=entry,
372 lower_case_key=lower_case_key))
373 else:
374 claim_value = self._parse_token_dict(
375 entry_number=None,
376 check_p_header=check_p_header,
377 token=token,
378 lower_case_key=lower_case_key)
379 return TokenItem(value=claim_value, claim_type=self)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200380
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200381
382 def _encode_dict(self, token_encoder, token_map, *, add_p_header, name_as_key, parse_raw_value):
383 token_encoder.encode_length(_CBOR_MAJOR_TYPE_MAP, len(token_map))
384 if name_as_key:
385 claims = {claim.get_claim_name().lower():
386 claim for claim in self._get_contained_claims()}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200387 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200388 claims = {claim.get_claim_key(): claim for claim in self._get_contained_claims()}
389 for key, val in token_map.items():
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200390 try:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200391 claim = claims[key]
392 key = claim.get_claim_key()
393 token_encoder.encode(key)
394 claim.convert_map_to_token(
395 token_encoder,
396 val,
397 add_p_header=add_p_header,
398 name_as_key=name_as_key,
399 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200400 except KeyError:
401 if self.config.strict:
402 if not self.config.keep_going:
403 raise
404 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200405 token_encoder.encode(key)
406 token_encoder.encode(val)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200407
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200408 def convert_map_to_token(
409 self,
410 token_encoder,
411 token_map,
412 *, add_p_header,
413 name_as_key,
414 parse_raw_value):
415 if self.is_list:
416 token_encoder.encode_length(_CBOR_MAJOR_TYPE_ARRAY, len(token_map))
417 for item in token_map:
418 self._encode_dict(
419 token_encoder,
420 item,
421 add_p_header=add_p_header,
422 name_as_key=name_as_key,
423 parse_raw_value=parse_raw_value)
424 else:
425 self._encode_dict(
426 token_encoder,
427 token_map,
428 add_p_header=add_p_header,
429 name_as_key=name_as_key,
430 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200431
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200432 def get_token_map(self, token_item):
433 if self.is_list:
434 ret = []
435 for token_item_dict in token_item.value:
436 token_dict = {}
437 for key, claim_token_item in token_item_dict.items():
438 token_dict[key] = claim_token_item.get_token_map()
439 ret.append(token_dict)
440 return ret
441 else:
442 token_dict = {}
443 for key, claim_token_item in token_item.value.items():
444 token_dict[key] = claim_token_item.get_token_map()
445 return token_dict
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200446
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200447@dataclass
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200448class VerifierConfiguration:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200449 """A class storing the configuration of the verifier.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200450
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200451 At the moment this determines what should happen if a problem is found
452 during verification.
453 """
454 keep_going: bool = False
455 strict: bool = False
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200456
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200457class AttestTokenRootClaims(CompositeAttestClaim):
458 """A claim type that is used to represent the claims in a token.
459
460 It is instantiated by AttestationTokenVerifier, and shouldn't be used
461 outside this module."""
462 def get_claim_key(self=None):
463 return None
464
465 def get_claim_name(self=None):
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200466 return "TOKEN_ROOT_CLAIMS"
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200467
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200468class AttestationTokenVerifier(AttestationClaim):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200469 """Abstract base class for attestation token verifiers"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200470
471 SIGN_METHOD_SIGN1 = "sign"
472 SIGN_METHOD_MAC0 = "mac"
473 SIGN_METHOD_RAW = "raw"
474
475 COSE_ALG_ES256="ES256"
476 COSE_ALG_ES384="ES384"
477 COSE_ALG_ES512="ES512"
478 COSE_ALG_HS256_64="HS256/64"
479 COSE_ALG_HS256="HS256"
480 COSE_ALG_HS384="HS384"
481 COSE_ALG_HS512="HS512"
482
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200483 @abstractmethod
484 def _get_p_header(self):
485 """Return the protected header for this Token
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200486
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200487 Return a dictionary if p_header should be present, and None if the token
488 doesn't defines a protected header.
489 """
490 raise NotImplementedError
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200491
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200492 @abstractmethod
493 def _get_wrapping_tag(self):
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200494 """The value of the tag that the token is wrapped in.
495
496 The function should return None if the token is not wrapped.
497 """
498 return None
499
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200500 @abstractmethod
501 def _parse_p_header(self, msg):
502 """Throw exception in case of error"""
503
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200504 def _get_cose_alg(self):
505 return self.cose_alg
506
507 def _get_method(self):
508 return self.method
509
510 def _get_signing_key(self):
511 return self.signing_key
512
513 def __init__(
514 self,
515 *, method,
516 cose_alg,
517 signing_key,
518 claims,
519 configuration=None,
520 necessity=AttestationClaim.MANDATORY):
521 self.method = method
522 self.cose_alg = cose_alg
523 self.signing_key=signing_key
524 self.config = configuration if configuration is not None else VerifierConfiguration()
525 self.seen_errors = False
526 self.claims = AttestTokenRootClaims(
527 verifier=self,
528 claims=claims,
529 is_list=False,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200530 necessity=necessity)
531
532 super().__init__(verifier=self, necessity=necessity)
533
534 def _sign_token(self, token, add_p_header):
535 """Signs a token"""
536 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
537 return token
538 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
539 return self._sign_eat(token, add_p_header)
540 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
541 return self._hmac_eat(token, add_p_header)
542 err_msg = 'Unexpected method "{}"; must be one of: raw, sign, mac'
543 raise ValueError(err_msg.format(self.method))
544
545 def _sign_eat(self, token, add_p_header):
546 protected_header = CoseAttrs()
547 p_header=self._get_p_header()
548 key=self._get_signing_key()
549 if add_p_header and p_header is not None and key:
550 protected_header.update(p_header)
551 signed_msg = Sign1Message(p_header=protected_header)
552 signed_msg.payload = token
553 if key:
554 signed_msg.key = key
555 signed_msg.signature = signed_msg.compute_signature(alg=self._get_cose_alg())
556 return signed_msg.encode()
557
558
559 def _hmac_eat(self, token, add_p_header):
560 protected_header = CoseAttrs()
561 p_header=self._get_p_header()
562 key=self._get_signing_key()
563 if add_p_header and p_header is not None and key:
564 protected_header.update(p_header)
565 hmac_msg = Mac0Message(payload=token, key=key, p_header=protected_header)
566 hmac_msg.compute_auth_tag(alg=self.cose_alg)
567 return hmac_msg.encode()
568
569
570 def _get_cose_sign1_payload(self, cose, *, check_p_header, verify_signature):
571 msg = Sign1Message.decode(cose)
572 if verify_signature:
573 key = self._get_signing_key()
574 if check_p_header:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200575 try:
576 self._parse_p_header(msg)
577 except Exception as exc:
578 self.error(f'Invalid Protected header: {exc}', exception=exc)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200579 msg.key = key
580 msg.signature = msg.signers
581 try:
582 msg.verify_signature(alg=self._get_cose_alg())
583 except Exception as exc:
584 raise ValueError(f'Bad signature ({exc})') from exc
585 return msg.payload
586
587
588 def _get_cose_mac0_payload(self, cose, *, check_p_header, verify_signature):
589 msg = Mac0Message.decode(cose)
590 if verify_signature:
591 key = self._get_signing_key()
592 if check_p_header:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200593 try:
594 self._parse_p_header(msg)
595 except Exception as exc:
596 self.error(f'Invalid Protected header: {exc}', exception=exc)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200597 msg.key = key
598 try:
599 msg.verify_auth_tag(alg=self._get_cose_alg())
600 except Exception as exc:
601 raise ValueError(f'Bad signature ({exc})') from exc
602 return msg.payload
603
604
605 def _get_cose_payload(self, cose, *, check_p_header, verify_signature):
606 """Return the payload of a COSE envelope"""
607 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
608 return self._get_cose_sign1_payload(
609 cose,
610 check_p_header=check_p_header,
611 verify_signature=verify_signature)
612 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
613 return self._get_cose_mac0_payload(
614 cose,
615 check_p_header=check_p_header,
616 verify_signature=verify_signature)
617 err_msg = f'Unexpected method "{self._get_method()}"; must be one of: sign, mac'
618 raise ValueError(err_msg)
619
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200620 def convert_map_to_token(
621 self,
622 token_encoder,
623 token_map,
624 *, add_p_header,
625 name_as_key,
626 parse_raw_value,
627 root=False):
628 with BytesIO() as b_io:
629 # Create a new encoder instance
630 encoder = CBOREncoder(b_io)
631
632 # Add tag if necessary
633 wrapping_tag = self._get_wrapping_tag()
634 if wrapping_tag is not None:
635 # TODO: this doesn't saves the string references used up to the
636 # point that this tag is added (see encode_semantic(...) in cbor2's
637 # encoder.py). This is not a problem as far the tokens don't use
638 # string references (which is the case for now).
639 encoder.encode_length(_CBOR_MAJOR_TYPE_SEMANTIC_TAG, wrapping_tag)
640
641 # Encode the token payload
642 self.claims.convert_map_to_token(
643 encoder,
644 token_map,
645 add_p_header=add_p_header,
646 name_as_key=name_as_key,
647 parse_raw_value=parse_raw_value)
648
649 token = b_io.getvalue()
650
651 # Sign and pack in a COSE envelope if necessary
652 signed_token = self._sign_token(token, add_p_header=add_p_header)
653
654 # Pack as a bstr if necessary
655 if root:
656 token_encoder.write(signed_token)
657 else:
658 token_encoder.encode_bytestring(signed_token)
659
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200660 def parse_token(self, *, token, check_p_header, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200661 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
662 payload = token
663 else:
664 try:
665 payload = self._get_cose_payload(
666 token,
667 check_p_header=check_p_header,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200668 # signature verification is done in the verify function
669 verify_signature=False)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200670 except Exception as exc:
671 msg = f'Bad COSE: {exc}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200672 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200673
674 try:
675 raw_map = cbor2.loads(payload)
676 except Exception as exc:
677 msg = f'Invalid CBOR: {exc}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200678 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200679
Mate Toth-Pale589c452022-07-27 22:02:40 +0200680 if isinstance(raw_map, _cbor2.CBORTag):
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200681 raw_map_tag = raw_map.tag
682 raw_map = raw_map.value
683 else:
684 raw_map_tag = None
685
686 token_items = self.claims.parse_token(
687 token=raw_map,
688 check_p_header=check_p_header,
689 lower_case_key=lower_case_key)
690
691 ret = TokenItem(value=token_items, claim_type=self)
692 ret.wrapping_tag = raw_map_tag
693 ret.token = token
694 ret.check_p_header = check_p_header
695 return ret
696
697 def verify(self, token_item):
698 if self._get_method() != AttestationTokenVerifier.SIGN_METHOD_RAW:
699 try:
700 self._get_cose_payload(
701 token_item.token,
702 check_p_header=token_item.check_p_header,
703 verify_signature=(self._get_signing_key() is not None))
704 except Exception as exc:
705 msg = f'Bad COSE: {exc}'
706 raise ValueError(msg) from exc
707
708 wrapping_tag = self._get_wrapping_tag()
709 if token_item.wrapping_tag is not None:
Mate Toth-Pale589c452022-07-27 22:02:40 +0200710 if wrapping_tag is None:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200711 msg = f'Invalid token: Unexpected tag (0x{token_item.wrapping_tag:x}) in token {self.get_claim_name()}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200712 self.error(msg)
Mate Toth-Pale589c452022-07-27 22:02:40 +0200713 else:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200714 if wrapping_tag != token_item.wrapping_tag:
715 msg = f'Invalid token: token {self.get_claim_name()} is wrapped in tag 0x{token_item.wrapping_tag:x} instead of 0x{wrapping_tag:x}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200716 self.error(msg)
Mate Toth-Pale589c452022-07-27 22:02:40 +0200717 else:
718 if wrapping_tag is not None:
719 msg = f'Invalid token: token {self.get_claim_name()} should be wrapped in tag 0x{wrapping_tag:x}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200720 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200721
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200722 token_item.value.verify()
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200723
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200724 def get_token_map(self, token_item):
725 return self.claims.get_token_map(token_item.value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200726
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200727 def error(self, message, *, exception=None):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200728 """Act on an error depending on the configuration of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200729 self.seen_errors = True
730 if self.config.keep_going:
731 logger.error(message)
732 else:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200733 if exception is None:
734 raise ValueError(message)
735 else:
736 raise ValueError(message) from Exception
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200737
738 def warning(self, message):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200739 """Print a warning with the logger of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200740 logger.warning(message)