blob: d442a0bb4710e203ffc03e81575a54bcd23571c8 [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-Pal642459f2022-10-07 11:15:45 +0200217 def _validate_bytestrings_equal(self, value, name, expected):
218 self._validate_bytestring_length_equals(value, name, len(expected))
219 for i, (b1, b2) in enumerate(zip(value, expected)):
220 if b1 != b2:
221 msg = f'Invalid {name} byte at {i}: 0x{b1:02x} instead of 0x{b2:02x}'
222 self.verifier.error(msg)
223
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200224 def get_token_map(self, token_item):
225 formatted = self.__class__.get_formatted_value(token_item.value)
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200226
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200227 # If the formatted value is still a bytestring then try to decode
228 if isinstance(formatted, bytes):
229 formatted = self.decode(formatted)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200230
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200231 return formatted
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200232
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200233class CompositeAttestClaim(AttestationClaim):
234 """
235 This class represents composite claim.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200236
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200237 This class is still abstract, but can contain other claims. This means that
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200238 a value representing this claim is a dictionary, or a list of dictionaries.
239 This claim contains further claims which represent the possible key-value
240 pairs in the value for this claim.
Mate Toth-Pal530106f2022-05-03 15:29:49 +0200241
242 It is possible that there are requirement that the claims in this claim must
243 satisfy, but this can't be checked in the `verify` function of a claim.
244
245 For example the composite claim can contain a claim type `A`, and a claim
246 type `B`, exactly one of the two can be present.
247
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200248 In this case the class inheriting from this class can have its own verify()
249 method.
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200250 """
251
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200252 def __init__(self,
253 *, verifier,
254 claims,
255 is_list,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200256 necessity=AttestationClaim.MANDATORY):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200257 """ Initialise a composite claim.
258
259 In case 'is_list' is False, the expected type of value is a dictionary,
260 containing the necessary claims determined by the 'claims' list.
261 In case 'is_list' is True, the expected type of value is a list,
262 containing a number of dictionaries, each one containing the necessary
263 claims determined by the 'claims' list.
264 """
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200265 super().__init__(verifier=verifier, necessity=necessity)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200266 self.is_list = is_list
267 self.claims = claims
268
269 def _get_contained_claims(self):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200270 for claim, args in self.claims:
271 try:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200272 yield claim(**args)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200273 except TypeError as exc:
274 raise TypeError(f"Failed to instantiate '{claim}' with args '{args}' in token " +
275 f"{type(self.verifier)}\nSee error in exception above.") from exc
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200276
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200277
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200278 def _verify_dict(self, claim_type, entry_number, dictionary):
279 if not isinstance(dictionary, dict):
280 if self.config.strict:
281 msg = 'The values in token {} must be a dict.'
282 self.verifier.error(msg.format(claim_type.get_claim_name()))
283 else:
284 msg = 'The values in token {} must be a dict, skipping'
285 self.verifier.warning(msg.format(claim_type.get_claim_name()))
286 return
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200287
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200288 claim_names = [val.get_claim_name() for val in claim_type._get_contained_claims()]
289 for claim_name, _ in dictionary.items():
290 if claim_name not in claim_names:
291 if self.config.strict:
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200292 msg = 'Unexpected {} claim: {}'
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200293 self.verifier.error(msg.format(claim_type.get_claim_name(), claim_name))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200294 else:
Mate Toth-Pal5ebca512022-03-24 16:45:51 +0100295 msg = 'Unexpected {} claim: {}, skipping.'
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200296 self.verifier.warning(msg.format(claim_type.get_claim_name(), claim_name))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200297 continue
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200298
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200299 claims = {val.get_claim_key(): val for val in claim_type._get_contained_claims()}
300 self._check_claims_necessity(entry_number, claims, dictionary)
301 for token_item in dictionary.values():
302 if isinstance(token_item, TokenItem):
303 token_item.verify()
304 else:
305 # the parse of this token item failed. So it cannot be verified.
306 # Warning had been reported during parsing.
307 pass
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200308
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200309 def verify(self, token_item):
310 if self.is_list:
311 if not isinstance(token_item.value, list):
312 if self.config.strict:
313 msg = 'The value of this token {} must be a list.'
314 self.verifier.error(msg.format(self.get_claim_name()))
315 else:
316 msg = 'The value of this token {} must be a list, skipping'
317 self.verifier.warning(msg.format(self.get_claim_name()))
318 return
319 for entry_number, list_item in enumerate(token_item.value):
320 self._verify_dict(token_item.claim_type, entry_number, list_item)
321 else:
322 self._verify_dict(token_item.claim_type, None, token_item.value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200323
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200324 def _parse_token_dict(self, *, entry_number, token, check_p_header, lower_case_key):
325 claim_value = {}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200326
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200327 if not isinstance(token, dict):
328 claim_value = token
329 else:
330 claims = {val.get_claim_key(): val for val in self._get_contained_claims()}
331 for key, val in token.items():
332 try:
333 claim = claims[key]
334 name = claim.get_claim_name()
335 if lower_case_key:
336 name = name.lower()
337 claim_value[name] = claim.parse_token(
338 token=val,
339 check_p_header=check_p_header,
340 lower_case_key=lower_case_key)
341 except KeyError:
342 claim_value[key] = val
343 except Exception:
344 if not self.config.keep_going:
345 raise
346 return claim_value
347
348 def _check_claims_necessity(self, entry_number, claims, dictionary):
349 mandatory_claim_names = [claim.get_claim_name() for claim in claims.values() if claim.necessity == AttestationClaim.MANDATORY]
350 recommended_claim_names = [claim.get_claim_name() for claim in claims.values() if claim.necessity == AttestationClaim.RECOMMENDED]
351 dictionary_claim_names = dictionary.keys()
352
353 for mandatory_claim_name in mandatory_claim_names:
354 if mandatory_claim_name not in dictionary_claim_names:
355 msg = (f'Invalid IAT: missing MANDATORY claim "{mandatory_claim_name}" '
356 f'from {self.get_claim_name()}')
357 if entry_number is not None:
358 msg += f' at index {entry_number}'
359 self.verifier.error(msg)
360
361 for recommended_claim_name in recommended_claim_names:
362 if recommended_claim_name not in dictionary_claim_names:
363 msg = (f'Missing RECOMMENDED claim "{recommended_claim_name}" '
364 f'from {self.get_claim_name()}')
365 if entry_number is not None:
366 msg += f' at index {entry_number}'
367 self.verifier.warning(msg)
368
369 def parse_token(self, *, token, check_p_header, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200370 """This expects a raw token map as 'token'"""
371
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200372 if self.is_list:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200373 claim_value = []
374 if not isinstance(token, list):
375 claim_value = token
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200376 else:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200377 for entry_number, entry in enumerate(token):
378 claim_value.append(self._parse_token_dict(
379 entry_number=entry_number,
380 check_p_header=check_p_header,
381 token=entry,
382 lower_case_key=lower_case_key))
383 else:
384 claim_value = self._parse_token_dict(
385 entry_number=None,
386 check_p_header=check_p_header,
387 token=token,
388 lower_case_key=lower_case_key)
389 return TokenItem(value=claim_value, claim_type=self)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200390
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200391
392 def _encode_dict(self, token_encoder, token_map, *, add_p_header, name_as_key, parse_raw_value):
393 token_encoder.encode_length(_CBOR_MAJOR_TYPE_MAP, len(token_map))
394 if name_as_key:
395 claims = {claim.get_claim_name().lower():
396 claim for claim in self._get_contained_claims()}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200397 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200398 claims = {claim.get_claim_key(): claim for claim in self._get_contained_claims()}
399 for key, val in token_map.items():
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200400 try:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200401 claim = claims[key]
402 key = claim.get_claim_key()
403 token_encoder.encode(key)
404 claim.convert_map_to_token(
405 token_encoder,
406 val,
407 add_p_header=add_p_header,
408 name_as_key=name_as_key,
409 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200410 except KeyError:
411 if self.config.strict:
412 if not self.config.keep_going:
413 raise
414 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200415 token_encoder.encode(key)
416 token_encoder.encode(val)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200417
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200418 def convert_map_to_token(
419 self,
420 token_encoder,
421 token_map,
422 *, add_p_header,
423 name_as_key,
424 parse_raw_value):
425 if self.is_list:
426 token_encoder.encode_length(_CBOR_MAJOR_TYPE_ARRAY, len(token_map))
427 for item in token_map:
428 self._encode_dict(
429 token_encoder,
430 item,
431 add_p_header=add_p_header,
432 name_as_key=name_as_key,
433 parse_raw_value=parse_raw_value)
434 else:
435 self._encode_dict(
436 token_encoder,
437 token_map,
438 add_p_header=add_p_header,
439 name_as_key=name_as_key,
440 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200441
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200442 def get_token_map(self, token_item):
443 if self.is_list:
444 ret = []
445 for token_item_dict in token_item.value:
446 token_dict = {}
447 for key, claim_token_item in token_item_dict.items():
Mate Toth-Palc9417662022-10-09 13:34:08 +0200448 if isinstance(claim_token_item, TokenItem):
449 token_dict[key] = claim_token_item.get_token_map()
450 else:
451 # The claim was not recognised, so just adding it as it
452 # was in the map:
453 token_dict[key] = claim_token_item
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200454 ret.append(token_dict)
455 return ret
456 else:
457 token_dict = {}
458 for key, claim_token_item in token_item.value.items():
Mate Toth-Palc9417662022-10-09 13:34:08 +0200459 if isinstance(claim_token_item, TokenItem):
460 token_dict[key] = claim_token_item.get_token_map()
461 else:
462 # The claim was not recognised, so just adding it as it
463 # was in the map:
464 token_dict[key] = claim_token_item
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200465 return token_dict
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200466
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200467@dataclass
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200468class VerifierConfiguration:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200469 """A class storing the configuration of the verifier.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200470
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200471 At the moment this determines what should happen if a problem is found
472 during verification.
473 """
474 keep_going: bool = False
475 strict: bool = False
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200476
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200477class AttestTokenRootClaims(CompositeAttestClaim):
478 """A claim type that is used to represent the claims in a token.
479
480 It is instantiated by AttestationTokenVerifier, and shouldn't be used
481 outside this module."""
482 def get_claim_key(self=None):
483 return None
484
485 def get_claim_name(self=None):
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200486 return "TOKEN_ROOT_CLAIMS"
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200487
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200488class AttestationTokenVerifier(AttestationClaim):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200489 """Abstract base class for attestation token verifiers"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200490
491 SIGN_METHOD_SIGN1 = "sign"
492 SIGN_METHOD_MAC0 = "mac"
493 SIGN_METHOD_RAW = "raw"
494
495 COSE_ALG_ES256="ES256"
496 COSE_ALG_ES384="ES384"
497 COSE_ALG_ES512="ES512"
498 COSE_ALG_HS256_64="HS256/64"
499 COSE_ALG_HS256="HS256"
500 COSE_ALG_HS384="HS384"
501 COSE_ALG_HS512="HS512"
502
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200503 @abstractmethod
504 def _get_p_header(self):
505 """Return the protected header for this Token
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200506
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200507 Return a dictionary if p_header should be present, and None if the token
508 doesn't defines a protected header.
509 """
510 raise NotImplementedError
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200511
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200512 @abstractmethod
513 def _get_wrapping_tag(self):
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200514 """The value of the tag that the token is wrapped in.
515
516 The function should return None if the token is not wrapped.
517 """
518 return None
519
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200520 @abstractmethod
521 def _parse_p_header(self, msg):
522 """Throw exception in case of error"""
523
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200524 def _get_cose_alg(self):
525 return self.cose_alg
526
527 def _get_method(self):
528 return self.method
529
530 def _get_signing_key(self):
531 return self.signing_key
532
533 def __init__(
534 self,
535 *, method,
536 cose_alg,
537 signing_key,
538 claims,
539 configuration=None,
540 necessity=AttestationClaim.MANDATORY):
541 self.method = method
542 self.cose_alg = cose_alg
543 self.signing_key=signing_key
544 self.config = configuration if configuration is not None else VerifierConfiguration()
545 self.seen_errors = False
546 self.claims = AttestTokenRootClaims(
547 verifier=self,
548 claims=claims,
549 is_list=False,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200550 necessity=necessity)
551
552 super().__init__(verifier=self, necessity=necessity)
553
554 def _sign_token(self, token, add_p_header):
555 """Signs a token"""
556 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
557 return token
558 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
559 return self._sign_eat(token, add_p_header)
560 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
561 return self._hmac_eat(token, add_p_header)
562 err_msg = 'Unexpected method "{}"; must be one of: raw, sign, mac'
563 raise ValueError(err_msg.format(self.method))
564
565 def _sign_eat(self, token, add_p_header):
566 protected_header = CoseAttrs()
567 p_header=self._get_p_header()
568 key=self._get_signing_key()
569 if add_p_header and p_header is not None and key:
570 protected_header.update(p_header)
571 signed_msg = Sign1Message(p_header=protected_header)
572 signed_msg.payload = token
573 if key:
574 signed_msg.key = key
575 signed_msg.signature = signed_msg.compute_signature(alg=self._get_cose_alg())
576 return signed_msg.encode()
577
578
579 def _hmac_eat(self, token, add_p_header):
580 protected_header = CoseAttrs()
581 p_header=self._get_p_header()
582 key=self._get_signing_key()
583 if add_p_header and p_header is not None and key:
584 protected_header.update(p_header)
585 hmac_msg = Mac0Message(payload=token, key=key, p_header=protected_header)
586 hmac_msg.compute_auth_tag(alg=self.cose_alg)
587 return hmac_msg.encode()
588
589
590 def _get_cose_sign1_payload(self, cose, *, check_p_header, verify_signature):
591 msg = Sign1Message.decode(cose)
592 if verify_signature:
593 key = self._get_signing_key()
594 if check_p_header:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200595 try:
596 self._parse_p_header(msg)
597 except Exception as exc:
598 self.error(f'Invalid Protected header: {exc}', exception=exc)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200599 msg.key = key
600 msg.signature = msg.signers
601 try:
602 msg.verify_signature(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_mac0_payload(self, cose, *, check_p_header, verify_signature):
609 msg = Mac0Message.decode(cose)
610 if verify_signature:
611 key = self._get_signing_key()
612 if check_p_header:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200613 try:
614 self._parse_p_header(msg)
615 except Exception as exc:
616 self.error(f'Invalid Protected header: {exc}', exception=exc)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200617 msg.key = key
618 try:
619 msg.verify_auth_tag(alg=self._get_cose_alg())
620 except Exception as exc:
621 raise ValueError(f'Bad signature ({exc})') from exc
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200622 return msg.payload, msg.protected_header
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200623
624
625 def _get_cose_payload(self, cose, *, check_p_header, verify_signature):
626 """Return the payload of a COSE envelope"""
627 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
628 return self._get_cose_sign1_payload(
629 cose,
630 check_p_header=check_p_header,
631 verify_signature=verify_signature)
632 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
633 return self._get_cose_mac0_payload(
634 cose,
635 check_p_header=check_p_header,
636 verify_signature=verify_signature)
637 err_msg = f'Unexpected method "{self._get_method()}"; must be one of: sign, mac'
638 raise ValueError(err_msg)
639
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200640 def convert_map_to_token(
641 self,
642 token_encoder,
643 token_map,
644 *, add_p_header,
645 name_as_key,
646 parse_raw_value,
647 root=False):
648 with BytesIO() as b_io:
649 # Create a new encoder instance
650 encoder = CBOREncoder(b_io)
651
652 # Add tag if necessary
653 wrapping_tag = self._get_wrapping_tag()
654 if wrapping_tag is not None:
655 # TODO: this doesn't saves the string references used up to the
656 # point that this tag is added (see encode_semantic(...) in cbor2's
657 # encoder.py). This is not a problem as far the tokens don't use
658 # string references (which is the case for now).
659 encoder.encode_length(_CBOR_MAJOR_TYPE_SEMANTIC_TAG, wrapping_tag)
660
661 # Encode the token payload
662 self.claims.convert_map_to_token(
663 encoder,
664 token_map,
665 add_p_header=add_p_header,
666 name_as_key=name_as_key,
667 parse_raw_value=parse_raw_value)
668
669 token = b_io.getvalue()
670
671 # Sign and pack in a COSE envelope if necessary
672 signed_token = self._sign_token(token, add_p_header=add_p_header)
673
674 # Pack as a bstr if necessary
675 if root:
676 token_encoder.write(signed_token)
677 else:
678 token_encoder.encode_bytestring(signed_token)
679
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200680 def parse_token(self, *, token, check_p_header, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200681 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
682 payload = token
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200683 protected_header = None
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200684 else:
685 try:
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200686 payload, protected_header = self._get_cose_payload(
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200687 token,
688 check_p_header=check_p_header,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200689 # signature verification is done in the verify function
690 verify_signature=False)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200691 except Exception as exc:
692 msg = f'Bad COSE: {exc}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200693 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200694
695 try:
696 raw_map = cbor2.loads(payload)
697 except Exception as exc:
698 msg = f'Invalid CBOR: {exc}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200699 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200700
Mate Toth-Pale589c452022-07-27 22:02:40 +0200701 if isinstance(raw_map, _cbor2.CBORTag):
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200702 raw_map_tag = raw_map.tag
703 raw_map = raw_map.value
704 else:
705 raw_map_tag = None
706
707 token_items = self.claims.parse_token(
708 token=raw_map,
709 check_p_header=check_p_header,
710 lower_case_key=lower_case_key)
711
712 ret = TokenItem(value=token_items, claim_type=self)
713 ret.wrapping_tag = raw_map_tag
714 ret.token = token
715 ret.check_p_header = check_p_header
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200716 ret.protected_header = protected_header
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200717 return ret
718
719 def verify(self, token_item):
720 if self._get_method() != AttestationTokenVerifier.SIGN_METHOD_RAW:
721 try:
722 self._get_cose_payload(
723 token_item.token,
724 check_p_header=token_item.check_p_header,
725 verify_signature=(self._get_signing_key() is not None))
726 except Exception as exc:
727 msg = f'Bad COSE: {exc}'
728 raise ValueError(msg) from exc
729
730 wrapping_tag = self._get_wrapping_tag()
731 if token_item.wrapping_tag is not None:
Mate Toth-Pale589c452022-07-27 22:02:40 +0200732 if wrapping_tag is None:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200733 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 +0200734 self.error(msg)
Mate Toth-Pale589c452022-07-27 22:02:40 +0200735 else:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200736 if wrapping_tag != token_item.wrapping_tag:
737 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 +0200738 self.error(msg)
Mate Toth-Pale589c452022-07-27 22:02:40 +0200739 else:
740 if wrapping_tag is not None:
741 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 +0200742 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200743
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200744 token_item.value.verify()
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200745
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200746 def get_token_map(self, token_item):
747 return self.claims.get_token_map(token_item.value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200748
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200749 def error(self, message, *, exception=None):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200750 """Act on an error depending on the configuration of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200751 self.seen_errors = True
752 if self.config.keep_going:
753 logger.error(message)
754 else:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200755 if exception is None:
756 raise ValueError(message)
757 else:
758 raise ValueError(message) from Exception
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200759
760 def warning(self, message):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200761 """Print a warning with the logger of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200762 logger.warning(message)