blob: 1480a9ab2a6c6bddeec02ab6ecee599cad9f84ea [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-Palb21ae522022-09-01 12:02:21 +020062 def __repr__(self):
63 return f"TokenItem({self.claim_type.__class__.__name__}, {self.value})"
64
Mate Toth-Pald10a9142022-04-28 15:34:13 +020065class AttestationClaim(ABC):
66 """
67 This class represents a claim.
68
69 This class is abstract. A concrete claim have to be derived from this class,
70 and it have to implement all the abstract methods.
71
72 This class contains methods that are not abstract. These are here as a
73 default behavior, that a derived class might either keep, or override.
Mate Toth-Pald10a9142022-04-28 15:34:13 +020074 """
75
Mate Toth-Palbb187d02022-04-26 16:01:51 +020076 MANDATORY = 0
77 RECOMMENDED = 1
78 OPTIONAL = 2
79
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020080 def __init__(self, *, verifier, necessity=MANDATORY):
Mate Toth-Palbb187d02022-04-26 16:01:51 +020081 self.config = verifier.config
82 self.verifier = verifier
83 self.necessity = necessity
Mate Toth-Palbb187d02022-04-26 16:01:51 +020084
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020085 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +020086 # Abstract methods
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020087 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +020088
89 @abstractmethod
Mate Toth-Palbb187d02022-04-26 16:01:51 +020090 def get_claim_key(self=None):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020091 """Get the key of this claim
92
93 Returns the key of this claim. The implementation have to support
94 calling this method with or without an instance as well."""
Mate Toth-Palbb187d02022-04-26 16:01:51 +020095 raise NotImplementedError
96
Mate Toth-Pald10a9142022-04-28 15:34:13 +020097 @abstractmethod
Mate Toth-Palbb187d02022-04-26 16:01:51 +020098 def get_claim_name(self=None):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020099 """Get the name of this claim
100
101 Returns the name of this claim. The implementation have to support
102 calling this method with or without an instance as well."""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200103 raise NotImplementedError
104
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200105 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200106 # Default methods that a derived class might override
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200107 #
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200108
109 def decode(self, value):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200110 """
111 Decode the value of the claim if the value is an UTF-8 string
112 """
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200113 if self.__class__.is_utf_8():
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200114 try:
115 return value.decode()
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200116 except UnicodeDecodeError as exc:
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200117 msg = 'Error decodeing value for "{}": {}'
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200118 self.verifier.error(msg.format(self.get_claim_name(), exc))
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200119 return str(value)[2:-1]
120 else: # not a UTF-8 value, i.e. a bytestring
121 return value
122
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200123 @classmethod
124 def is_utf_8(cls):
125 """Returns whether the value of this claim should be UTF-8"""
126 return False
127
128 def convert_map_to_token(self,
129 token_encoder,
130 token_map,
131 *, add_p_header,
132 name_as_key,
133 parse_raw_value):
134 """Encode a map in cbor format using the 'token_encoder'"""
135 # pylint: disable=unused-argument
136 value = token_map
137 if parse_raw_value:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200138 value = self.__class__.parse_raw(value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200139 return token_encoder.encode(value)
140
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200141 def parse_token(self, *, token, check_p_header, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200142 """Parse a token into a map
143
144 This function is recursive for composite claims and for token verifiers.
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200145
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200146 The `token` parameter can be interpreted differently in derived classes:
147 - as a raw token that is decoded by the CBOR parsing library
148 - as CBOR encoded token in case of (nested) tokens.
149 """
150 return TokenItem(value=token, claim_type=self)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200151
152 @classmethod
153 def parse_raw(cls, raw_value):
154 """Parse a raw value
155
156 Takes a string, as it appears in a yaml file, and converts it to a
157 numeric value according to the claim's definition.
158 """
159 return raw_value
160
161 @classmethod
162 def get_formatted_value(cls, value):
163 """Format the value according to this claim"""
164 if cls.is_utf_8():
165 # this is an UTF-8 value, force string type
166 return f'{value}'
167 return value
168
169 #
170 # Helper functions to be called from derived classes
171 #
172
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200173 def _check_type(self, name, value, expected_type):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200174 """Check that a value's type is as expected"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200175 if not isinstance(value, expected_type):
176 msg = 'Invalid {}: must be a(n) {}: found {}'
177 self.verifier.error(msg.format(name, expected_type, type(value)))
178 return False
179 return True
180
181 def _validate_bytestring_length_equals(self, value, name, expected_len):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200182 """Check that a bytestring length is as expected"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200183 self._check_type(name, value, bytes)
184
185 value_len = len(value)
186 if value_len != expected_len:
187 msg = 'Invalid {} length: must be exactly {} bytes, found {} bytes'
188 self.verifier.error(msg.format(name, expected_len, value_len))
189
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200190 def _validate_bytestring_length_one_of(self, value, name, possible_lens):
191 """Check that a bytestring length is as expected"""
192 self._check_type(name, value, bytes)
193
194 value_len = len(value)
195 if value_len not in possible_lens:
196 msg = 'Invalid {} length: must be one of {} bytes, found {} bytes'
197 self.verifier.error(msg.format(name, possible_lens, value_len))
198
199 def _validate_bytestring_length_between(self, value, name, min_len, max_len):
200 """Check that a bytestring length is as expected"""
201 self._check_type(name, value, bytes)
202
203 value_len = len(value)
204 if value_len < min_len or value_len > max_len:
205 msg = 'Invalid {} length: must be between {} and {} bytes, found {} bytes'
206 self.verifier.error(msg.format(name, min_len, max_len, value_len))
207
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200208 def _validate_bytestring_length_is_at_least(self, value, name, minimal_length):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200209 """Check that a bytestring has a minimum length"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200210 self._check_type(name, value, bytes)
211
212 value_len = len(value)
213 if value_len < minimal_length:
214 msg = 'Invalid {} length: must be at least {} bytes, found {} bytes'
215 self.verifier.error(msg.format(name, minimal_length, value_len))
216
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200217 def get_token_map(self, token_item):
218 formatted = self.__class__.get_formatted_value(token_item.value)
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200219
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200220 # If the formatted value is still a bytestring then try to decode
221 if isinstance(formatted, bytes):
222 formatted = self.decode(formatted)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200223
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200224 return formatted
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200225
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200226class CompositeAttestClaim(AttestationClaim):
227 """
228 This class represents composite claim.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200229
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200230 This class is still abstract, but can contain other claims. This means that
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200231 a value representing this claim is a dictionary, or a list of dictionaries.
232 This claim contains further claims which represent the possible key-value
233 pairs in the value for this claim.
Mate Toth-Pal530106f2022-05-03 15:29:49 +0200234
235 It is possible that there are requirement that the claims in this claim must
236 satisfy, but this can't be checked in the `verify` function of a claim.
237
238 For example the composite claim can contain a claim type `A`, and a claim
239 type `B`, exactly one of the two can be present.
240
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200241 In this case the class inheriting from this class can have its own verify()
242 method.
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200243 """
244
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200245 def __init__(self,
246 *, verifier,
247 claims,
248 is_list,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200249 necessity=AttestationClaim.MANDATORY):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200250 """ Initialise a composite claim.
251
252 In case 'is_list' is False, the expected type of value is a dictionary,
253 containing the necessary claims determined by the 'claims' list.
254 In case 'is_list' is True, the expected type of value is a list,
255 containing a number of dictionaries, each one containing the necessary
256 claims determined by the 'claims' list.
257 """
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200258 super().__init__(verifier=verifier, necessity=necessity)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200259 self.is_list = is_list
260 self.claims = claims
261
262 def _get_contained_claims(self):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200263 for claim, args in self.claims:
264 try:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200265 yield claim(**args)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200266 except TypeError as exc:
267 raise TypeError(f"Failed to instantiate '{claim}' with args '{args}' in token " +
268 f"{type(self.verifier)}\nSee error in exception above.") from exc
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200269
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200270
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200271 def _verify_dict(self, claim_type, entry_number, dictionary):
272 if not isinstance(dictionary, dict):
273 if self.config.strict:
274 msg = 'The values in token {} must be a dict.'
275 self.verifier.error(msg.format(claim_type.get_claim_name()))
276 else:
277 msg = 'The values in token {} must be a dict, skipping'
278 self.verifier.warning(msg.format(claim_type.get_claim_name()))
279 return
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200280
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200281 claim_names = [val.get_claim_name() for val in claim_type._get_contained_claims()]
282 for claim_name, _ in dictionary.items():
283 if claim_name not in claim_names:
284 if self.config.strict:
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200285 msg = 'Unexpected {} claim: {}'
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200286 self.verifier.error(msg.format(claim_type.get_claim_name(), claim_name))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200287 else:
Mate Toth-Pal5ebca512022-03-24 16:45:51 +0100288 msg = 'Unexpected {} claim: {}, skipping.'
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200289 self.verifier.warning(msg.format(claim_type.get_claim_name(), claim_name))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200290 continue
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200291
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200292 claims = {val.get_claim_key(): val for val in claim_type._get_contained_claims()}
293 self._check_claims_necessity(entry_number, claims, dictionary)
294 for token_item in dictionary.values():
295 if isinstance(token_item, TokenItem):
296 token_item.verify()
297 else:
298 # the parse of this token item failed. So it cannot be verified.
299 # Warning had been reported during parsing.
300 pass
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200301
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200302 def verify(self, token_item):
303 if self.is_list:
304 if not isinstance(token_item.value, list):
305 if self.config.strict:
306 msg = 'The value of this token {} must be a list.'
307 self.verifier.error(msg.format(self.get_claim_name()))
308 else:
309 msg = 'The value of this token {} must be a list, skipping'
310 self.verifier.warning(msg.format(self.get_claim_name()))
311 return
312 for entry_number, list_item in enumerate(token_item.value):
313 self._verify_dict(token_item.claim_type, entry_number, list_item)
314 else:
315 self._verify_dict(token_item.claim_type, None, token_item.value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200316
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200317 def _parse_token_dict(self, *, entry_number, token, check_p_header, lower_case_key):
318 claim_value = {}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200319
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200320 if not isinstance(token, dict):
321 claim_value = token
322 else:
323 claims = {val.get_claim_key(): val for val in self._get_contained_claims()}
324 for key, val in token.items():
325 try:
326 claim = claims[key]
327 name = claim.get_claim_name()
328 if lower_case_key:
329 name = name.lower()
330 claim_value[name] = claim.parse_token(
331 token=val,
332 check_p_header=check_p_header,
333 lower_case_key=lower_case_key)
334 except KeyError:
335 claim_value[key] = val
336 except Exception:
337 if not self.config.keep_going:
338 raise
339 return claim_value
340
341 def _check_claims_necessity(self, entry_number, claims, dictionary):
342 mandatory_claim_names = [claim.get_claim_name() for claim in claims.values() if claim.necessity == AttestationClaim.MANDATORY]
343 recommended_claim_names = [claim.get_claim_name() for claim in claims.values() if claim.necessity == AttestationClaim.RECOMMENDED]
344 dictionary_claim_names = dictionary.keys()
345
346 for mandatory_claim_name in mandatory_claim_names:
347 if mandatory_claim_name not in dictionary_claim_names:
348 msg = (f'Invalid IAT: missing MANDATORY claim "{mandatory_claim_name}" '
349 f'from {self.get_claim_name()}')
350 if entry_number is not None:
351 msg += f' at index {entry_number}'
352 self.verifier.error(msg)
353
354 for recommended_claim_name in recommended_claim_names:
355 if recommended_claim_name not in dictionary_claim_names:
356 msg = (f'Missing RECOMMENDED claim "{recommended_claim_name}" '
357 f'from {self.get_claim_name()}')
358 if entry_number is not None:
359 msg += f' at index {entry_number}'
360 self.verifier.warning(msg)
361
362 def parse_token(self, *, token, check_p_header, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200363 """This expects a raw token map as 'token'"""
364
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200365 if self.is_list:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200366 claim_value = []
367 if not isinstance(token, list):
368 claim_value = token
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200369 else:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200370 for entry_number, entry in enumerate(token):
371 claim_value.append(self._parse_token_dict(
372 entry_number=entry_number,
373 check_p_header=check_p_header,
374 token=entry,
375 lower_case_key=lower_case_key))
376 else:
377 claim_value = self._parse_token_dict(
378 entry_number=None,
379 check_p_header=check_p_header,
380 token=token,
381 lower_case_key=lower_case_key)
382 return TokenItem(value=claim_value, claim_type=self)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200383
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200384
385 def _encode_dict(self, token_encoder, token_map, *, add_p_header, name_as_key, parse_raw_value):
386 token_encoder.encode_length(_CBOR_MAJOR_TYPE_MAP, len(token_map))
387 if name_as_key:
388 claims = {claim.get_claim_name().lower():
389 claim for claim in self._get_contained_claims()}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200390 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200391 claims = {claim.get_claim_key(): claim for claim in self._get_contained_claims()}
392 for key, val in token_map.items():
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200393 try:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200394 claim = claims[key]
395 key = claim.get_claim_key()
396 token_encoder.encode(key)
397 claim.convert_map_to_token(
398 token_encoder,
399 val,
400 add_p_header=add_p_header,
401 name_as_key=name_as_key,
402 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200403 except KeyError:
404 if self.config.strict:
405 if not self.config.keep_going:
406 raise
407 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200408 token_encoder.encode(key)
409 token_encoder.encode(val)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200410
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200411 def convert_map_to_token(
412 self,
413 token_encoder,
414 token_map,
415 *, add_p_header,
416 name_as_key,
417 parse_raw_value):
418 if self.is_list:
419 token_encoder.encode_length(_CBOR_MAJOR_TYPE_ARRAY, len(token_map))
420 for item in token_map:
421 self._encode_dict(
422 token_encoder,
423 item,
424 add_p_header=add_p_header,
425 name_as_key=name_as_key,
426 parse_raw_value=parse_raw_value)
427 else:
428 self._encode_dict(
429 token_encoder,
430 token_map,
431 add_p_header=add_p_header,
432 name_as_key=name_as_key,
433 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200434
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200435 def get_token_map(self, token_item):
436 if self.is_list:
437 ret = []
438 for token_item_dict in token_item.value:
439 token_dict = {}
440 for key, claim_token_item in token_item_dict.items():
441 token_dict[key] = claim_token_item.get_token_map()
442 ret.append(token_dict)
443 return ret
444 else:
445 token_dict = {}
446 for key, claim_token_item in token_item.value.items():
447 token_dict[key] = claim_token_item.get_token_map()
448 return token_dict
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200449
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200450@dataclass
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200451class VerifierConfiguration:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200452 """A class storing the configuration of the verifier.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200453
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200454 At the moment this determines what should happen if a problem is found
455 during verification.
456 """
457 keep_going: bool = False
458 strict: bool = False
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200459
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200460class AttestTokenRootClaims(CompositeAttestClaim):
461 """A claim type that is used to represent the claims in a token.
462
463 It is instantiated by AttestationTokenVerifier, and shouldn't be used
464 outside this module."""
465 def get_claim_key(self=None):
466 return None
467
468 def get_claim_name(self=None):
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200469 return "TOKEN_ROOT_CLAIMS"
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200470
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200471class AttestationTokenVerifier(AttestationClaim):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200472 """Abstract base class for attestation token verifiers"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200473
474 SIGN_METHOD_SIGN1 = "sign"
475 SIGN_METHOD_MAC0 = "mac"
476 SIGN_METHOD_RAW = "raw"
477
478 COSE_ALG_ES256="ES256"
479 COSE_ALG_ES384="ES384"
480 COSE_ALG_ES512="ES512"
481 COSE_ALG_HS256_64="HS256/64"
482 COSE_ALG_HS256="HS256"
483 COSE_ALG_HS384="HS384"
484 COSE_ALG_HS512="HS512"
485
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200486 @abstractmethod
487 def _get_p_header(self):
488 """Return the protected header for this Token
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200489
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200490 Return a dictionary if p_header should be present, and None if the token
491 doesn't defines a protected header.
492 """
493 raise NotImplementedError
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200494
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200495 @abstractmethod
496 def _get_wrapping_tag(self):
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200497 """The value of the tag that the token is wrapped in.
498
499 The function should return None if the token is not wrapped.
500 """
501 return None
502
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200503 @abstractmethod
504 def _parse_p_header(self, msg):
505 """Throw exception in case of error"""
506
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200507 def _get_cose_alg(self):
508 return self.cose_alg
509
510 def _get_method(self):
511 return self.method
512
513 def _get_signing_key(self):
514 return self.signing_key
515
516 def __init__(
517 self,
518 *, method,
519 cose_alg,
520 signing_key,
521 claims,
522 configuration=None,
523 necessity=AttestationClaim.MANDATORY):
524 self.method = method
525 self.cose_alg = cose_alg
526 self.signing_key=signing_key
527 self.config = configuration if configuration is not None else VerifierConfiguration()
528 self.seen_errors = False
529 self.claims = AttestTokenRootClaims(
530 verifier=self,
531 claims=claims,
532 is_list=False,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200533 necessity=necessity)
534
535 super().__init__(verifier=self, necessity=necessity)
536
537 def _sign_token(self, token, add_p_header):
538 """Signs a token"""
539 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
540 return token
541 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
542 return self._sign_eat(token, add_p_header)
543 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
544 return self._hmac_eat(token, add_p_header)
545 err_msg = 'Unexpected method "{}"; must be one of: raw, sign, mac'
546 raise ValueError(err_msg.format(self.method))
547
548 def _sign_eat(self, token, add_p_header):
549 protected_header = CoseAttrs()
550 p_header=self._get_p_header()
551 key=self._get_signing_key()
552 if add_p_header and p_header is not None and key:
553 protected_header.update(p_header)
554 signed_msg = Sign1Message(p_header=protected_header)
555 signed_msg.payload = token
556 if key:
557 signed_msg.key = key
558 signed_msg.signature = signed_msg.compute_signature(alg=self._get_cose_alg())
559 return signed_msg.encode()
560
561
562 def _hmac_eat(self, token, add_p_header):
563 protected_header = CoseAttrs()
564 p_header=self._get_p_header()
565 key=self._get_signing_key()
566 if add_p_header and p_header is not None and key:
567 protected_header.update(p_header)
568 hmac_msg = Mac0Message(payload=token, key=key, p_header=protected_header)
569 hmac_msg.compute_auth_tag(alg=self.cose_alg)
570 return hmac_msg.encode()
571
572
573 def _get_cose_sign1_payload(self, cose, *, check_p_header, verify_signature):
574 msg = Sign1Message.decode(cose)
575 if verify_signature:
576 key = self._get_signing_key()
577 if check_p_header:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200578 try:
579 self._parse_p_header(msg)
580 except Exception as exc:
581 self.error(f'Invalid Protected header: {exc}', exception=exc)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200582 msg.key = key
583 msg.signature = msg.signers
584 try:
585 msg.verify_signature(alg=self._get_cose_alg())
586 except Exception as exc:
587 raise ValueError(f'Bad signature ({exc})') from exc
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200588 return msg.payload, msg.protected_header
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200589
590
591 def _get_cose_mac0_payload(self, cose, *, check_p_header, verify_signature):
592 msg = Mac0Message.decode(cose)
593 if verify_signature:
594 key = self._get_signing_key()
595 if check_p_header:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200596 try:
597 self._parse_p_header(msg)
598 except Exception as exc:
599 self.error(f'Invalid Protected header: {exc}', exception=exc)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200600 msg.key = key
601 try:
602 msg.verify_auth_tag(alg=self._get_cose_alg())
603 except Exception as exc:
604 raise ValueError(f'Bad signature ({exc})') from exc
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200605 return msg.payload, msg.protected_header
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200606
607
608 def _get_cose_payload(self, cose, *, check_p_header, verify_signature):
609 """Return the payload of a COSE envelope"""
610 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
611 return self._get_cose_sign1_payload(
612 cose,
613 check_p_header=check_p_header,
614 verify_signature=verify_signature)
615 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
616 return self._get_cose_mac0_payload(
617 cose,
618 check_p_header=check_p_header,
619 verify_signature=verify_signature)
620 err_msg = f'Unexpected method "{self._get_method()}"; must be one of: sign, mac'
621 raise ValueError(err_msg)
622
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200623 def convert_map_to_token(
624 self,
625 token_encoder,
626 token_map,
627 *, add_p_header,
628 name_as_key,
629 parse_raw_value,
630 root=False):
631 with BytesIO() as b_io:
632 # Create a new encoder instance
633 encoder = CBOREncoder(b_io)
634
635 # Add tag if necessary
636 wrapping_tag = self._get_wrapping_tag()
637 if wrapping_tag is not None:
638 # TODO: this doesn't saves the string references used up to the
639 # point that this tag is added (see encode_semantic(...) in cbor2's
640 # encoder.py). This is not a problem as far the tokens don't use
641 # string references (which is the case for now).
642 encoder.encode_length(_CBOR_MAJOR_TYPE_SEMANTIC_TAG, wrapping_tag)
643
644 # Encode the token payload
645 self.claims.convert_map_to_token(
646 encoder,
647 token_map,
648 add_p_header=add_p_header,
649 name_as_key=name_as_key,
650 parse_raw_value=parse_raw_value)
651
652 token = b_io.getvalue()
653
654 # Sign and pack in a COSE envelope if necessary
655 signed_token = self._sign_token(token, add_p_header=add_p_header)
656
657 # Pack as a bstr if necessary
658 if root:
659 token_encoder.write(signed_token)
660 else:
661 token_encoder.encode_bytestring(signed_token)
662
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200663 def parse_token(self, *, token, check_p_header, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200664 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
665 payload = token
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200666 protected_header = None
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200667 else:
668 try:
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200669 payload, protected_header = self._get_cose_payload(
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200670 token,
671 check_p_header=check_p_header,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200672 # signature verification is done in the verify function
673 verify_signature=False)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200674 except Exception as exc:
675 msg = f'Bad COSE: {exc}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200676 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200677
678 try:
679 raw_map = cbor2.loads(payload)
680 except Exception as exc:
681 msg = f'Invalid CBOR: {exc}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200682 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200683
Mate Toth-Pale589c452022-07-27 22:02:40 +0200684 if isinstance(raw_map, _cbor2.CBORTag):
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200685 raw_map_tag = raw_map.tag
686 raw_map = raw_map.value
687 else:
688 raw_map_tag = None
689
690 token_items = self.claims.parse_token(
691 token=raw_map,
692 check_p_header=check_p_header,
693 lower_case_key=lower_case_key)
694
695 ret = TokenItem(value=token_items, claim_type=self)
696 ret.wrapping_tag = raw_map_tag
697 ret.token = token
698 ret.check_p_header = check_p_header
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200699 ret.protected_header = protected_header
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200700 return ret
701
702 def verify(self, token_item):
703 if self._get_method() != AttestationTokenVerifier.SIGN_METHOD_RAW:
704 try:
705 self._get_cose_payload(
706 token_item.token,
707 check_p_header=token_item.check_p_header,
708 verify_signature=(self._get_signing_key() is not None))
709 except Exception as exc:
710 msg = f'Bad COSE: {exc}'
711 raise ValueError(msg) from exc
712
713 wrapping_tag = self._get_wrapping_tag()
714 if token_item.wrapping_tag is not None:
Mate Toth-Pale589c452022-07-27 22:02:40 +0200715 if wrapping_tag is None:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200716 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 +0200717 self.error(msg)
Mate Toth-Pale589c452022-07-27 22:02:40 +0200718 else:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200719 if wrapping_tag != token_item.wrapping_tag:
720 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 +0200721 self.error(msg)
Mate Toth-Pale589c452022-07-27 22:02:40 +0200722 else:
723 if wrapping_tag is not None:
724 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 +0200725 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200726
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200727 token_item.value.verify()
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200728
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200729 def get_token_map(self, token_item):
730 return self.claims.get_token_map(token_item.value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200731
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200732 def error(self, message, *, exception=None):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200733 """Act on an error depending on the configuration of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200734 self.seen_errors = True
735 if self.config.keep_going:
736 logger.error(message)
737 else:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200738 if exception is None:
739 raise ValueError(message)
740 else:
741 raise ValueError(message) from Exception
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200742
743 def warning(self, message):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200744 """Print a warning with the logger of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200745 logger.warning(message)