blob: 98b162fbee105d465bfb2c43824901eab133b490 [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,
Mate Toth-Pale305e552022-10-07 14:04:53 +0200131 *,name_as_key,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200132 parse_raw_value):
133 """Encode a map in cbor format using the 'token_encoder'"""
134 # pylint: disable=unused-argument
135 value = token_map
136 if parse_raw_value:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200137 value = self.__class__.parse_raw(value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200138 return token_encoder.encode(value)
139
Mate Toth-Pale305e552022-10-07 14:04:53 +0200140 def parse_token(self, *, token, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200141 """Parse a token into a map
142
143 This function is recursive for composite claims and for token verifiers.
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200144
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200145 The `token` parameter can be interpreted differently in derived classes:
146 - as a raw token that is decoded by the CBOR parsing library
147 - as CBOR encoded token in case of (nested) tokens.
148 """
149 return TokenItem(value=token, claim_type=self)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200150
151 @classmethod
152 def parse_raw(cls, raw_value):
153 """Parse a raw value
154
155 Takes a string, as it appears in a yaml file, and converts it to a
156 numeric value according to the claim's definition.
157 """
158 return raw_value
159
160 @classmethod
161 def get_formatted_value(cls, value):
162 """Format the value according to this claim"""
163 if cls.is_utf_8():
164 # this is an UTF-8 value, force string type
165 return f'{value}'
166 return value
167
168 #
169 # Helper functions to be called from derived classes
170 #
171
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200172 def _check_type(self, name, value, expected_type):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200173 """Check that a value's type is as expected"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200174 if not isinstance(value, expected_type):
175 msg = 'Invalid {}: must be a(n) {}: found {}'
176 self.verifier.error(msg.format(name, expected_type, type(value)))
177 return False
178 return True
179
180 def _validate_bytestring_length_equals(self, value, name, expected_len):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200181 """Check that a bytestring length is as expected"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200182 self._check_type(name, value, bytes)
183
184 value_len = len(value)
185 if value_len != expected_len:
186 msg = 'Invalid {} length: must be exactly {} bytes, found {} bytes'
187 self.verifier.error(msg.format(name, expected_len, value_len))
188
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200189 def _validate_bytestring_length_one_of(self, value, name, possible_lens):
190 """Check that a bytestring length is as expected"""
191 self._check_type(name, value, bytes)
192
193 value_len = len(value)
194 if value_len not in possible_lens:
195 msg = 'Invalid {} length: must be one of {} bytes, found {} bytes'
196 self.verifier.error(msg.format(name, possible_lens, value_len))
197
198 def _validate_bytestring_length_between(self, value, name, min_len, max_len):
199 """Check that a bytestring length is as expected"""
200 self._check_type(name, value, bytes)
201
202 value_len = len(value)
203 if value_len < min_len or value_len > max_len:
204 msg = 'Invalid {} length: must be between {} and {} bytes, found {} bytes'
205 self.verifier.error(msg.format(name, min_len, max_len, value_len))
206
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200207 def _validate_bytestring_length_is_at_least(self, value, name, minimal_length):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200208 """Check that a bytestring has a minimum length"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200209 self._check_type(name, value, bytes)
210
211 value_len = len(value)
212 if value_len < minimal_length:
213 msg = 'Invalid {} length: must be at least {} bytes, found {} bytes'
214 self.verifier.error(msg.format(name, minimal_length, value_len))
215
Mate Toth-Pal642459f2022-10-07 11:15:45 +0200216 def _validate_bytestrings_equal(self, value, name, expected):
217 self._validate_bytestring_length_equals(value, name, len(expected))
218 for i, (b1, b2) in enumerate(zip(value, expected)):
219 if b1 != b2:
220 msg = f'Invalid {name} byte at {i}: 0x{b1:02x} instead of 0x{b2:02x}'
221 self.verifier.error(msg)
222
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200223 def get_token_map(self, token_item):
224 formatted = self.__class__.get_formatted_value(token_item.value)
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200225
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200226 # If the formatted value is still a bytestring then try to decode
227 if isinstance(formatted, bytes):
228 formatted = self.decode(formatted)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200229
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200230 return formatted
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200231
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200232class CompositeAttestClaim(AttestationClaim):
233 """
234 This class represents composite claim.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200235
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200236 This class is still abstract, but can contain other claims. This means that
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200237 a value representing this claim is a dictionary, or a list of dictionaries.
238 This claim contains further claims which represent the possible key-value
239 pairs in the value for this claim.
Mate Toth-Pal530106f2022-05-03 15:29:49 +0200240
241 It is possible that there are requirement that the claims in this claim must
242 satisfy, but this can't be checked in the `verify` function of a claim.
243
244 For example the composite claim can contain a claim type `A`, and a claim
245 type `B`, exactly one of the two can be present.
246
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200247 In this case the class inheriting from this class can have its own verify()
248 method.
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200249 """
250
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200251 def __init__(self,
252 *, verifier,
253 claims,
254 is_list,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200255 necessity=AttestationClaim.MANDATORY):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200256 """ Initialise a composite claim.
257
258 In case 'is_list' is False, the expected type of value is a dictionary,
259 containing the necessary claims determined by the 'claims' list.
260 In case 'is_list' is True, the expected type of value is a list,
261 containing a number of dictionaries, each one containing the necessary
262 claims determined by the 'claims' list.
263 """
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200264 super().__init__(verifier=verifier, necessity=necessity)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200265 self.is_list = is_list
266 self.claims = claims
267
268 def _get_contained_claims(self):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200269 for claim, args in self.claims:
270 try:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200271 yield claim(**args)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200272 except TypeError as exc:
273 raise TypeError(f"Failed to instantiate '{claim}' with args '{args}' in token " +
274 f"{type(self.verifier)}\nSee error in exception above.") from exc
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200275
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200276
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200277 def _verify_dict(self, claim_type, entry_number, dictionary):
278 if not isinstance(dictionary, dict):
279 if self.config.strict:
280 msg = 'The values in token {} must be a dict.'
281 self.verifier.error(msg.format(claim_type.get_claim_name()))
282 else:
283 msg = 'The values in token {} must be a dict, skipping'
284 self.verifier.warning(msg.format(claim_type.get_claim_name()))
285 return
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200286
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200287 claim_names = [val.get_claim_name() for val in claim_type._get_contained_claims()]
288 for claim_name, _ in dictionary.items():
289 if claim_name not in claim_names:
290 if self.config.strict:
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200291 msg = 'Unexpected {} claim: {}'
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200292 self.verifier.error(msg.format(claim_type.get_claim_name(), claim_name))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200293 else:
Mate Toth-Pal5ebca512022-03-24 16:45:51 +0100294 msg = 'Unexpected {} claim: {}, skipping.'
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200295 self.verifier.warning(msg.format(claim_type.get_claim_name(), claim_name))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200296 continue
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200297
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200298 claims = {val.get_claim_key(): val for val in claim_type._get_contained_claims()}
299 self._check_claims_necessity(entry_number, claims, dictionary)
300 for token_item in dictionary.values():
301 if isinstance(token_item, TokenItem):
302 token_item.verify()
303 else:
304 # the parse of this token item failed. So it cannot be verified.
305 # Warning had been reported during parsing.
306 pass
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200307
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200308 def verify(self, token_item):
309 if self.is_list:
310 if not isinstance(token_item.value, list):
311 if self.config.strict:
312 msg = 'The value of this token {} must be a list.'
313 self.verifier.error(msg.format(self.get_claim_name()))
314 else:
315 msg = 'The value of this token {} must be a list, skipping'
316 self.verifier.warning(msg.format(self.get_claim_name()))
317 return
318 for entry_number, list_item in enumerate(token_item.value):
319 self._verify_dict(token_item.claim_type, entry_number, list_item)
320 else:
321 self._verify_dict(token_item.claim_type, None, token_item.value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200322
Mate Toth-Pale305e552022-10-07 14:04:53 +0200323 def _parse_token_dict(self, *, entry_number, token, lower_case_key):
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200324 claim_value = {}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200325
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200326 if not isinstance(token, dict):
327 claim_value = token
328 else:
329 claims = {val.get_claim_key(): val for val in self._get_contained_claims()}
330 for key, val in token.items():
331 try:
332 claim = claims[key]
333 name = claim.get_claim_name()
334 if lower_case_key:
335 name = name.lower()
336 claim_value[name] = claim.parse_token(
337 token=val,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200338 lower_case_key=lower_case_key)
339 except KeyError:
340 claim_value[key] = val
341 except Exception:
342 if not self.config.keep_going:
343 raise
344 return claim_value
345
346 def _check_claims_necessity(self, entry_number, claims, dictionary):
347 mandatory_claim_names = [claim.get_claim_name() for claim in claims.values() if claim.necessity == AttestationClaim.MANDATORY]
348 recommended_claim_names = [claim.get_claim_name() for claim in claims.values() if claim.necessity == AttestationClaim.RECOMMENDED]
349 dictionary_claim_names = dictionary.keys()
350
351 for mandatory_claim_name in mandatory_claim_names:
352 if mandatory_claim_name not in dictionary_claim_names:
353 msg = (f'Invalid IAT: missing MANDATORY claim "{mandatory_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.error(msg)
358
359 for recommended_claim_name in recommended_claim_names:
360 if recommended_claim_name not in dictionary_claim_names:
361 msg = (f'Missing RECOMMENDED claim "{recommended_claim_name}" '
362 f'from {self.get_claim_name()}')
363 if entry_number is not None:
364 msg += f' at index {entry_number}'
365 self.verifier.warning(msg)
366
Mate Toth-Pale305e552022-10-07 14:04:53 +0200367 def parse_token(self, *, token, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200368 """This expects a raw token map as 'token'"""
369
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200370 if self.is_list:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200371 claim_value = []
372 if not isinstance(token, list):
373 claim_value = token
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200374 else:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200375 for entry_number, entry in enumerate(token):
376 claim_value.append(self._parse_token_dict(
377 entry_number=entry_number,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200378 token=entry,
379 lower_case_key=lower_case_key))
380 else:
381 claim_value = self._parse_token_dict(
382 entry_number=None,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200383 token=token,
384 lower_case_key=lower_case_key)
385 return TokenItem(value=claim_value, claim_type=self)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200386
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200387
Mate Toth-Pale305e552022-10-07 14:04:53 +0200388 def _encode_dict(self, token_encoder, token_map, *, name_as_key, parse_raw_value):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200389 token_encoder.encode_length(_CBOR_MAJOR_TYPE_MAP, len(token_map))
390 if name_as_key:
391 claims = {claim.get_claim_name().lower():
392 claim for claim in self._get_contained_claims()}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200393 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200394 claims = {claim.get_claim_key(): claim for claim in self._get_contained_claims()}
395 for key, val in token_map.items():
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200396 try:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200397 claim = claims[key]
398 key = claim.get_claim_key()
399 token_encoder.encode(key)
400 claim.convert_map_to_token(
401 token_encoder,
402 val,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200403 name_as_key=name_as_key,
404 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200405 except KeyError:
406 if self.config.strict:
407 if not self.config.keep_going:
408 raise
409 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200410 token_encoder.encode(key)
411 token_encoder.encode(val)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200412
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200413 def convert_map_to_token(
414 self,
415 token_encoder,
416 token_map,
Mate Toth-Pale305e552022-10-07 14:04:53 +0200417 *, name_as_key,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200418 parse_raw_value):
419 if self.is_list:
420 token_encoder.encode_length(_CBOR_MAJOR_TYPE_ARRAY, len(token_map))
421 for item in token_map:
422 self._encode_dict(
423 token_encoder,
424 item,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200425 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,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200431 name_as_key=name_as_key,
432 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200433
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200434 def get_token_map(self, token_item):
435 if self.is_list:
436 ret = []
437 for token_item_dict in token_item.value:
438 token_dict = {}
439 for key, claim_token_item in token_item_dict.items():
Mate Toth-Palc9417662022-10-09 13:34:08 +0200440 if isinstance(claim_token_item, TokenItem):
441 token_dict[key] = claim_token_item.get_token_map()
442 else:
443 # The claim was not recognised, so just adding it as it
444 # was in the map:
445 token_dict[key] = claim_token_item
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200446 ret.append(token_dict)
447 return ret
448 else:
449 token_dict = {}
450 for key, claim_token_item in token_item.value.items():
Mate Toth-Palc9417662022-10-09 13:34:08 +0200451 if isinstance(claim_token_item, TokenItem):
452 token_dict[key] = claim_token_item.get_token_map()
453 else:
454 # The claim was not recognised, so just adding it as it
455 # was in the map:
456 token_dict[key] = claim_token_item
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200457 return token_dict
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200458
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200459@dataclass
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200460class VerifierConfiguration:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200461 """A class storing the configuration of the verifier.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200462
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200463 At the moment this determines what should happen if a problem is found
464 during verification.
465 """
466 keep_going: bool = False
467 strict: bool = False
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200468
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200469class AttestTokenRootClaims(CompositeAttestClaim):
470 """A claim type that is used to represent the claims in a token.
471
472 It is instantiated by AttestationTokenVerifier, and shouldn't be used
473 outside this module."""
474 def get_claim_key(self=None):
475 return None
476
477 def get_claim_name(self=None):
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200478 return "TOKEN_ROOT_CLAIMS"
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200479
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200480class AttestationTokenVerifier(AttestationClaim):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200481 """Abstract base class for attestation token verifiers"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200482
483 SIGN_METHOD_SIGN1 = "sign"
484 SIGN_METHOD_MAC0 = "mac"
485 SIGN_METHOD_RAW = "raw"
486
487 COSE_ALG_ES256="ES256"
488 COSE_ALG_ES384="ES384"
489 COSE_ALG_ES512="ES512"
490 COSE_ALG_HS256_64="HS256/64"
491 COSE_ALG_HS256="HS256"
492 COSE_ALG_HS384="HS384"
493 COSE_ALG_HS512="HS512"
494
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200495 @abstractmethod
496 def _get_p_header(self):
497 """Return the protected header for this Token
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200498
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200499 Return a dictionary if p_header should be present, and None if the token
500 doesn't defines a protected header.
501 """
502 raise NotImplementedError
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200503
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200504 @abstractmethod
505 def _get_wrapping_tag(self):
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200506 """The value of the tag that the token is wrapped in.
507
508 The function should return None if the token is not wrapped.
509 """
510 return None
511
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200512 @abstractmethod
513 def _parse_p_header(self, msg):
514 """Throw exception in case of error"""
515
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200516 def _get_cose_alg(self):
517 return self.cose_alg
518
519 def _get_method(self):
520 return self.method
521
522 def _get_signing_key(self):
523 return self.signing_key
524
525 def __init__(
526 self,
527 *, method,
528 cose_alg,
529 signing_key,
530 claims,
531 configuration=None,
532 necessity=AttestationClaim.MANDATORY):
533 self.method = method
534 self.cose_alg = cose_alg
535 self.signing_key=signing_key
536 self.config = configuration if configuration is not None else VerifierConfiguration()
537 self.seen_errors = False
538 self.claims = AttestTokenRootClaims(
539 verifier=self,
540 claims=claims,
541 is_list=False,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200542 necessity=necessity)
543
544 super().__init__(verifier=self, necessity=necessity)
545
Mate Toth-Pale305e552022-10-07 14:04:53 +0200546 def _sign_token(self, token):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200547 """Signs a token"""
548 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
549 return token
550 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
Mate Toth-Pale305e552022-10-07 14:04:53 +0200551 return self._sign_eat(token)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200552 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
Mate Toth-Pale305e552022-10-07 14:04:53 +0200553 return self._hmac_eat(token)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200554 err_msg = 'Unexpected method "{}"; must be one of: raw, sign, mac'
555 raise ValueError(err_msg.format(self.method))
556
Mate Toth-Pale305e552022-10-07 14:04:53 +0200557 def _sign_eat(self, token):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200558 protected_header = CoseAttrs()
559 p_header=self._get_p_header()
560 key=self._get_signing_key()
Mate Toth-Pale305e552022-10-07 14:04:53 +0200561 if p_header is not None and key:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200562 protected_header.update(p_header)
563 signed_msg = Sign1Message(p_header=protected_header)
564 signed_msg.payload = token
565 if key:
566 signed_msg.key = key
567 signed_msg.signature = signed_msg.compute_signature(alg=self._get_cose_alg())
568 return signed_msg.encode()
569
570
Mate Toth-Pale305e552022-10-07 14:04:53 +0200571 def _hmac_eat(self, token):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200572 protected_header = CoseAttrs()
573 p_header=self._get_p_header()
574 key=self._get_signing_key()
Mate Toth-Pale305e552022-10-07 14:04:53 +0200575 if p_header is not None and key:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200576 protected_header.update(p_header)
577 hmac_msg = Mac0Message(payload=token, key=key, p_header=protected_header)
578 hmac_msg.compute_auth_tag(alg=self.cose_alg)
579 return hmac_msg.encode()
580
581
Mate Toth-Pale305e552022-10-07 14:04:53 +0200582 def _get_cose_sign1_payload(self, cose, *, verify_signature):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200583 msg = Sign1Message.decode(cose)
584 if verify_signature:
585 key = self._get_signing_key()
Mate Toth-Pale305e552022-10-07 14:04:53 +0200586 try:
587 self._parse_p_header(msg)
588 except Exception as exc:
589 self.error(f'Invalid Protected header: {exc}', exception=exc)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200590 msg.key = key
591 msg.signature = msg.signers
592 try:
593 msg.verify_signature(alg=self._get_cose_alg())
594 except Exception as exc:
595 raise ValueError(f'Bad signature ({exc})') from exc
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200596 return msg.payload, msg.protected_header
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200597
598
Mate Toth-Pale305e552022-10-07 14:04:53 +0200599 def _get_cose_mac0_payload(self, cose, *, verify_signature):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200600 msg = Mac0Message.decode(cose)
601 if verify_signature:
602 key = self._get_signing_key()
Mate Toth-Pale305e552022-10-07 14:04:53 +0200603 try:
604 self._parse_p_header(msg)
605 except Exception as exc:
606 self.error(f'Invalid Protected header: {exc}', exception=exc)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200607 msg.key = key
608 try:
609 msg.verify_auth_tag(alg=self._get_cose_alg())
610 except Exception as exc:
611 raise ValueError(f'Bad signature ({exc})') from exc
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200612 return msg.payload, msg.protected_header
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200613
614
Mate Toth-Pale305e552022-10-07 14:04:53 +0200615 def _get_cose_payload(self, cose, *, verify_signature):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200616 """Return the payload of a COSE envelope"""
617 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
618 return self._get_cose_sign1_payload(
619 cose,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200620 verify_signature=verify_signature)
621 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
622 return self._get_cose_mac0_payload(
623 cose,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200624 verify_signature=verify_signature)
625 err_msg = f'Unexpected method "{self._get_method()}"; must be one of: sign, mac'
626 raise ValueError(err_msg)
627
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200628 def convert_map_to_token(
629 self,
630 token_encoder,
631 token_map,
Mate Toth-Pale305e552022-10-07 14:04:53 +0200632 *, name_as_key,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200633 parse_raw_value,
634 root=False):
635 with BytesIO() as b_io:
636 # Create a new encoder instance
637 encoder = CBOREncoder(b_io)
638
639 # Add tag if necessary
640 wrapping_tag = self._get_wrapping_tag()
641 if wrapping_tag is not None:
642 # TODO: this doesn't saves the string references used up to the
643 # point that this tag is added (see encode_semantic(...) in cbor2's
644 # encoder.py). This is not a problem as far the tokens don't use
645 # string references (which is the case for now).
646 encoder.encode_length(_CBOR_MAJOR_TYPE_SEMANTIC_TAG, wrapping_tag)
647
648 # Encode the token payload
649 self.claims.convert_map_to_token(
650 encoder,
651 token_map,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200652 name_as_key=name_as_key,
653 parse_raw_value=parse_raw_value)
654
655 token = b_io.getvalue()
656
657 # Sign and pack in a COSE envelope if necessary
Mate Toth-Pale305e552022-10-07 14:04:53 +0200658 signed_token = self._sign_token(token)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200659
660 # Pack as a bstr if necessary
661 if root:
662 token_encoder.write(signed_token)
663 else:
664 token_encoder.encode_bytestring(signed_token)
665
Mate Toth-Pale305e552022-10-07 14:04:53 +0200666 def parse_token(self, *, token, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200667 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
668 payload = token
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200669 protected_header = None
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200670 else:
671 try:
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200672 payload, protected_header = self._get_cose_payload(
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200673 token,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200674 # signature verification is done in the verify function
675 verify_signature=False)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200676 except Exception as exc:
677 msg = f'Bad COSE: {exc}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200678 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200679
680 try:
681 raw_map = cbor2.loads(payload)
682 except Exception as exc:
683 msg = f'Invalid CBOR: {exc}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200684 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200685
Mate Toth-Pale589c452022-07-27 22:02:40 +0200686 if isinstance(raw_map, _cbor2.CBORTag):
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200687 raw_map_tag = raw_map.tag
688 raw_map = raw_map.value
689 else:
690 raw_map_tag = None
691
692 token_items = self.claims.parse_token(
693 token=raw_map,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200694 lower_case_key=lower_case_key)
695
696 ret = TokenItem(value=token_items, claim_type=self)
697 ret.wrapping_tag = raw_map_tag
698 ret.token = token
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,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200707 verify_signature=(self._get_signing_key() is not None))
708 except Exception as exc:
709 msg = f'Bad COSE: {exc}'
710 raise ValueError(msg) from exc
711
712 wrapping_tag = self._get_wrapping_tag()
713 if token_item.wrapping_tag is not None:
Mate Toth-Pale589c452022-07-27 22:02:40 +0200714 if wrapping_tag is None:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200715 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 +0200716 self.error(msg)
Mate Toth-Pale589c452022-07-27 22:02:40 +0200717 else:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200718 if wrapping_tag != token_item.wrapping_tag:
719 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 +0200720 self.error(msg)
Mate Toth-Pale589c452022-07-27 22:02:40 +0200721 else:
722 if wrapping_tag is not None:
723 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 +0200724 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200725
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200726 token_item.value.verify()
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200727
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200728 def get_token_map(self, token_item):
729 return self.claims.get_token_map(token_item.value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200730
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200731 def error(self, message, *, exception=None):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200732 """Act on an error depending on the configuration of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200733 self.seen_errors = True
734 if self.config.keep_going:
735 logger.error(message)
736 else:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200737 if exception is None:
738 raise ValueError(message)
739 else:
740 raise ValueError(message) from Exception
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200741
742 def warning(self, message):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200743 """Print a warning with the logger of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200744 logger.warning(message)