blob: beed4050644e3465bc012bae04f06f3d57506620 [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
Thomas Fossatif4e1ca32024-08-16 16:01:31 +000018from pycose.messages.sign1message import Sign1Message
19from pycose.messages.mac0message import Mac0Message
Mate Toth-Palbb187d02022-04-26 16:01:51 +020020
21import cbor2
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020022from cbor2 import CBOREncoder
Mate Toth-Pale589c452022-07-27 22:02:40 +020023import _cbor2
Mate Toth-Palbb187d02022-04-26 16:01:51 +020024
25logger = logging.getLogger('iat-verifiers')
26
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020027_CBOR_MAJOR_TYPE_ARRAY = 4
28_CBOR_MAJOR_TYPE_MAP = 5
29_CBOR_MAJOR_TYPE_SEMANTIC_TAG = 6
30
Mate Toth-Palc7404e92022-07-15 11:11:13 +020031class TokenItem:
32 """This class represents an item in the token map
33
34 The Field `claim_type` contains an AttestationClaim object, that determines how to interpret the
35 `value` field.
36 The field `value` contains either another TokenItem object or a representation of a claim value
37 (list, dictionary, bytestring...) depending on the value of `claim_type`
38
39 A TokenItem object might have extra fields beyond these as it might be necessary to store
40 properties during parsing, that can aid verifying.
41 """
42 def __init__(self, *, value, claim_type):
43 self.value = value # The value of the claim
44 self.claim_type = claim_type # an AttestationClaim instance
45
46 @classmethod
47 def _call_verify_with_parents(cls, claim_type_class, claim_type, token_item, indent):
48 for parent_class in claim_type_class.__bases__:
49 cls._call_verify_with_parents(parent_class, claim_type, token_item, indent + 2)
50 if "verify" in claim_type_class.__dict__:
51 claim_type_class.verify(claim_type, token_item)
52
53 def verify(self):
54 """Calls claim_type's and its parents' verify method"""
55 claim_type = self.claim_type
56 self.__class__._call_verify_with_parents(claim_type.__class__, claim_type, self, 0)
57
58 def get_token_map(self):
59 return self.claim_type.get_token_map(self)
60
Mate Toth-Palb21ae522022-09-01 12:02:21 +020061 def __repr__(self):
62 return f"TokenItem({self.claim_type.__class__.__name__}, {self.value})"
63
Mate Toth-Pald10a9142022-04-28 15:34:13 +020064class AttestationClaim(ABC):
65 """
66 This class represents a claim.
67
68 This class is abstract. A concrete claim have to be derived from this class,
69 and it have to implement all the abstract methods.
70
71 This class contains methods that are not abstract. These are here as a
72 default behavior, that a derived class might either keep, or override.
Mate Toth-Pald10a9142022-04-28 15:34:13 +020073 """
74
Mate Toth-Palbb187d02022-04-26 16:01:51 +020075 MANDATORY = 0
76 RECOMMENDED = 1
77 OPTIONAL = 2
78
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020079 def __init__(self, *, verifier, necessity=MANDATORY):
Mate Toth-Palbb187d02022-04-26 16:01:51 +020080 self.config = verifier.config
81 self.verifier = verifier
82 self.necessity = necessity
Mate Toth-Palbb187d02022-04-26 16:01:51 +020083
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020084 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +020085 # Abstract methods
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020086 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +020087
88 @abstractmethod
Mate Toth-Palbb187d02022-04-26 16:01:51 +020089 def get_claim_key(self=None):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020090 """Get the key of this claim
91
92 Returns the key of this claim. The implementation have to support
93 calling this method with or without an instance as well."""
Mate Toth-Palbb187d02022-04-26 16:01:51 +020094 raise NotImplementedError
95
Mate Toth-Pald10a9142022-04-28 15:34:13 +020096 @abstractmethod
Mate Toth-Palbb187d02022-04-26 16:01:51 +020097 def get_claim_name(self=None):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020098 """Get the name of this claim
99
100 Returns the name of this claim. The implementation have to support
101 calling this method with or without an instance as well."""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200102 raise NotImplementedError
103
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200104 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200105 # Default methods that a derived class might override
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200106 #
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200107
108 def decode(self, value):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200109 """
110 Decode the value of the claim if the value is an UTF-8 string
111 """
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200112 if self.__class__.is_utf_8():
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200113 try:
114 return value.decode()
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200115 except UnicodeDecodeError as exc:
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200116 msg = 'Error decodeing value for "{}": {}'
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200117 self.verifier.error(msg.format(self.get_claim_name(), exc))
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200118 return str(value)[2:-1]
119 else: # not a UTF-8 value, i.e. a bytestring
120 return value
121
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200122 @classmethod
123 def is_utf_8(cls):
124 """Returns whether the value of this claim should be UTF-8"""
125 return False
126
127 def convert_map_to_token(self,
128 token_encoder,
129 token_map,
Mate Toth-Pale305e552022-10-07 14:04:53 +0200130 *,name_as_key,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200131 parse_raw_value):
132 """Encode a map in cbor format using the 'token_encoder'"""
133 # pylint: disable=unused-argument
134 value = token_map
135 if parse_raw_value:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200136 value = self.__class__.parse_raw(value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200137 return token_encoder.encode(value)
138
Mate Toth-Pale305e552022-10-07 14:04:53 +0200139 def parse_token(self, *, token, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200140 """Parse a token into a map
141
142 This function is recursive for composite claims and for token verifiers.
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200143
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200144 The `token` parameter can be interpreted differently in derived classes:
145 - as a raw token that is decoded by the CBOR parsing library
146 - as CBOR encoded token in case of (nested) tokens.
147 """
148 return TokenItem(value=token, claim_type=self)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200149
150 @classmethod
151 def parse_raw(cls, raw_value):
152 """Parse a raw value
153
154 Takes a string, as it appears in a yaml file, and converts it to a
155 numeric value according to the claim's definition.
156 """
157 return raw_value
158
159 @classmethod
160 def get_formatted_value(cls, value):
161 """Format the value according to this claim"""
162 if cls.is_utf_8():
163 # this is an UTF-8 value, force string type
164 return f'{value}'
165 return value
166
167 #
168 # Helper functions to be called from derived classes
169 #
170
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200171 def _check_type(self, name, value, expected_type):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200172 """Check that a value's type is as expected"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200173 if not isinstance(value, expected_type):
174 msg = 'Invalid {}: must be a(n) {}: found {}'
175 self.verifier.error(msg.format(name, expected_type, type(value)))
176 return False
177 return True
178
179 def _validate_bytestring_length_equals(self, value, name, expected_len):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200180 """Check that a bytestring length is as expected"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200181 self._check_type(name, value, bytes)
182
183 value_len = len(value)
184 if value_len != expected_len:
185 msg = 'Invalid {} length: must be exactly {} bytes, found {} bytes'
186 self.verifier.error(msg.format(name, expected_len, value_len))
187
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200188 def _validate_bytestring_length_one_of(self, value, name, possible_lens):
189 """Check that a bytestring length is as expected"""
190 self._check_type(name, value, bytes)
191
192 value_len = len(value)
193 if value_len not in possible_lens:
194 msg = 'Invalid {} length: must be one of {} bytes, found {} bytes'
195 self.verifier.error(msg.format(name, possible_lens, value_len))
196
197 def _validate_bytestring_length_between(self, value, name, min_len, max_len):
198 """Check that a bytestring length is as expected"""
199 self._check_type(name, value, bytes)
200
201 value_len = len(value)
202 if value_len < min_len or value_len > max_len:
203 msg = 'Invalid {} length: must be between {} and {} bytes, found {} bytes'
204 self.verifier.error(msg.format(name, min_len, max_len, value_len))
205
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200206 def _validate_bytestring_length_is_at_least(self, value, name, minimal_length):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200207 """Check that a bytestring has a minimum length"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200208 self._check_type(name, value, bytes)
209
210 value_len = len(value)
211 if value_len < minimal_length:
212 msg = 'Invalid {} length: must be at least {} bytes, found {} bytes'
213 self.verifier.error(msg.format(name, minimal_length, value_len))
214
Mate Toth-Pal642459f2022-10-07 11:15:45 +0200215 def _validate_bytestrings_equal(self, value, name, expected):
216 self._validate_bytestring_length_equals(value, name, len(expected))
217 for i, (b1, b2) in enumerate(zip(value, expected)):
218 if b1 != b2:
219 msg = f'Invalid {name} byte at {i}: 0x{b1:02x} instead of 0x{b2:02x}'
220 self.verifier.error(msg)
221
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200222 def get_token_map(self, token_item):
223 formatted = self.__class__.get_formatted_value(token_item.value)
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200224
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200225 # If the formatted value is still a bytestring then try to decode
226 if isinstance(formatted, bytes):
227 formatted = self.decode(formatted)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200228
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200229 return formatted
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200230
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200231class CompositeAttestClaim(AttestationClaim):
232 """
233 This class represents composite claim.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200234
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200235 This class is still abstract, but can contain other claims. This means that
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200236 a value representing this claim is a dictionary, or a list of dictionaries.
237 This claim contains further claims which represent the possible key-value
238 pairs in the value for this claim.
Mate Toth-Pal530106f2022-05-03 15:29:49 +0200239
240 It is possible that there are requirement that the claims in this claim must
241 satisfy, but this can't be checked in the `verify` function of a claim.
242
243 For example the composite claim can contain a claim type `A`, and a claim
244 type `B`, exactly one of the two can be present.
245
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200246 In this case the class inheriting from this class can have its own verify()
247 method.
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200248 """
249
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200250 def __init__(self,
251 *, verifier,
252 claims,
253 is_list,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200254 necessity=AttestationClaim.MANDATORY):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200255 """ Initialise a composite claim.
256
257 In case 'is_list' is False, the expected type of value is a dictionary,
258 containing the necessary claims determined by the 'claims' list.
259 In case 'is_list' is True, the expected type of value is a list,
260 containing a number of dictionaries, each one containing the necessary
261 claims determined by the 'claims' list.
262 """
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200263 super().__init__(verifier=verifier, necessity=necessity)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200264 self.is_list = is_list
265 self.claims = claims
266
267 def _get_contained_claims(self):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200268 for claim, args in self.claims:
269 try:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200270 yield claim(**args)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200271 except TypeError as exc:
272 raise TypeError(f"Failed to instantiate '{claim}' with args '{args}' in token " +
273 f"{type(self.verifier)}\nSee error in exception above.") from exc
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200274
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200275
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200276 def _verify_dict(self, claim_type, entry_number, dictionary):
277 if not isinstance(dictionary, dict):
278 if self.config.strict:
279 msg = 'The values in token {} must be a dict.'
280 self.verifier.error(msg.format(claim_type.get_claim_name()))
281 else:
282 msg = 'The values in token {} must be a dict, skipping'
283 self.verifier.warning(msg.format(claim_type.get_claim_name()))
284 return
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200285
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200286 claim_names = [val.get_claim_name() for val in claim_type._get_contained_claims()]
287 for claim_name, _ in dictionary.items():
288 if claim_name not in claim_names:
289 if self.config.strict:
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200290 msg = 'Unexpected {} claim: {}'
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200291 self.verifier.error(msg.format(claim_type.get_claim_name(), claim_name))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200292 else:
Mate Toth-Pal5ebca512022-03-24 16:45:51 +0100293 msg = 'Unexpected {} claim: {}, skipping.'
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200294 self.verifier.warning(msg.format(claim_type.get_claim_name(), claim_name))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200295 continue
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200296
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200297 claims = {val.get_claim_key(): val for val in claim_type._get_contained_claims()}
298 self._check_claims_necessity(entry_number, claims, dictionary)
299 for token_item in dictionary.values():
300 if isinstance(token_item, TokenItem):
301 token_item.verify()
302 else:
303 # the parse of this token item failed. So it cannot be verified.
304 # Warning had been reported during parsing.
305 pass
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200306
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200307 def verify(self, token_item):
308 if self.is_list:
309 if not isinstance(token_item.value, list):
310 if self.config.strict:
311 msg = 'The value of this token {} must be a list.'
312 self.verifier.error(msg.format(self.get_claim_name()))
313 else:
314 msg = 'The value of this token {} must be a list, skipping'
315 self.verifier.warning(msg.format(self.get_claim_name()))
316 return
317 for entry_number, list_item in enumerate(token_item.value):
318 self._verify_dict(token_item.claim_type, entry_number, list_item)
319 else:
320 self._verify_dict(token_item.claim_type, None, token_item.value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200321
Mate Toth-Pale305e552022-10-07 14:04:53 +0200322 def _parse_token_dict(self, *, entry_number, token, lower_case_key):
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200323 claim_value = {}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200324
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200325 if not isinstance(token, dict):
326 claim_value = token
327 else:
328 claims = {val.get_claim_key(): val for val in self._get_contained_claims()}
329 for key, val in token.items():
330 try:
331 claim = claims[key]
332 name = claim.get_claim_name()
333 if lower_case_key:
334 name = name.lower()
335 claim_value[name] = claim.parse_token(
336 token=val,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200337 lower_case_key=lower_case_key)
338 except KeyError:
339 claim_value[key] = val
340 except Exception:
341 if not self.config.keep_going:
342 raise
343 return claim_value
344
345 def _check_claims_necessity(self, entry_number, claims, dictionary):
346 mandatory_claim_names = [claim.get_claim_name() for claim in claims.values() if claim.necessity == AttestationClaim.MANDATORY]
347 recommended_claim_names = [claim.get_claim_name() for claim in claims.values() if claim.necessity == AttestationClaim.RECOMMENDED]
348 dictionary_claim_names = dictionary.keys()
349
350 for mandatory_claim_name in mandatory_claim_names:
351 if mandatory_claim_name not in dictionary_claim_names:
352 msg = (f'Invalid IAT: missing MANDATORY claim "{mandatory_claim_name}" '
353 f'from {self.get_claim_name()}')
354 if entry_number is not None:
355 msg += f' at index {entry_number}'
356 self.verifier.error(msg)
357
358 for recommended_claim_name in recommended_claim_names:
359 if recommended_claim_name not in dictionary_claim_names:
360 msg = (f'Missing RECOMMENDED claim "{recommended_claim_name}" '
361 f'from {self.get_claim_name()}')
362 if entry_number is not None:
363 msg += f' at index {entry_number}'
364 self.verifier.warning(msg)
365
Mate Toth-Pale305e552022-10-07 14:04:53 +0200366 def parse_token(self, *, token, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200367 """This expects a raw token map as 'token'"""
368
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200369 if self.is_list:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200370 claim_value = []
371 if not isinstance(token, list):
372 claim_value = token
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200373 else:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200374 for entry_number, entry in enumerate(token):
375 claim_value.append(self._parse_token_dict(
376 entry_number=entry_number,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200377 token=entry,
378 lower_case_key=lower_case_key))
379 else:
380 claim_value = self._parse_token_dict(
381 entry_number=None,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200382 token=token,
383 lower_case_key=lower_case_key)
384 return TokenItem(value=claim_value, claim_type=self)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200385
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200386
Mate Toth-Pale305e552022-10-07 14:04:53 +0200387 def _encode_dict(self, token_encoder, token_map, *, name_as_key, parse_raw_value):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200388 token_encoder.encode_length(_CBOR_MAJOR_TYPE_MAP, len(token_map))
389 if name_as_key:
390 claims = {claim.get_claim_name().lower():
391 claim for claim in self._get_contained_claims()}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200392 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200393 claims = {claim.get_claim_key(): claim for claim in self._get_contained_claims()}
394 for key, val in token_map.items():
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200395 try:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200396 claim = claims[key]
397 key = claim.get_claim_key()
398 token_encoder.encode(key)
399 claim.convert_map_to_token(
400 token_encoder,
401 val,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200402 name_as_key=name_as_key,
403 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200404 except KeyError:
405 if self.config.strict:
406 if not self.config.keep_going:
407 raise
408 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200409 token_encoder.encode(key)
410 token_encoder.encode(val)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200411
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200412 def convert_map_to_token(
413 self,
414 token_encoder,
415 token_map,
Mate Toth-Pale305e552022-10-07 14:04:53 +0200416 *, name_as_key,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200417 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,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200424 name_as_key=name_as_key,
425 parse_raw_value=parse_raw_value)
426 else:
427 self._encode_dict(
428 token_encoder,
429 token_map,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200430 name_as_key=name_as_key,
431 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200432
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200433 def get_token_map(self, token_item):
434 if self.is_list:
435 ret = []
436 for token_item_dict in token_item.value:
437 token_dict = {}
438 for key, claim_token_item in token_item_dict.items():
Mate Toth-Palc9417662022-10-09 13:34:08 +0200439 if isinstance(claim_token_item, TokenItem):
440 token_dict[key] = claim_token_item.get_token_map()
441 else:
442 # The claim was not recognised, so just adding it as it
443 # was in the map:
444 token_dict[key] = claim_token_item
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200445 ret.append(token_dict)
446 return ret
447 else:
448 token_dict = {}
449 for key, claim_token_item in token_item.value.items():
Mate Toth-Palc9417662022-10-09 13:34:08 +0200450 if isinstance(claim_token_item, TokenItem):
451 token_dict[key] = claim_token_item.get_token_map()
452 else:
453 # The claim was not recognised, so just adding it as it
454 # was in the map:
455 token_dict[key] = claim_token_item
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200456 return token_dict
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200457
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200458@dataclass
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200459class VerifierConfiguration:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200460 """A class storing the configuration of the verifier.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200461
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200462 At the moment this determines what should happen if a problem is found
463 during verification.
464 """
465 keep_going: bool = False
466 strict: bool = False
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200467
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200468class AttestTokenRootClaims(CompositeAttestClaim):
469 """A claim type that is used to represent the claims in a token.
470
471 It is instantiated by AttestationTokenVerifier, and shouldn't be used
472 outside this module."""
473 def get_claim_key(self=None):
474 return None
475
476 def get_claim_name(self=None):
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200477 return "TOKEN_ROOT_CLAIMS"
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200478
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200479class AttestationTokenVerifier(AttestationClaim):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200480 """Abstract base class for attestation token verifiers"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200481
482 SIGN_METHOD_SIGN1 = "sign"
483 SIGN_METHOD_MAC0 = "mac"
484 SIGN_METHOD_RAW = "raw"
485
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200486 @abstractmethod
487 def _get_p_header(self):
Thomas Fossatif4e1ca32024-08-16 16:01:31 +0000488 """Return the protected header for this Token"""
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200489 raise NotImplementedError
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200490
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200491 @abstractmethod
492 def _get_wrapping_tag(self):
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200493 """The value of the tag that the token is wrapped in.
494
495 The function should return None if the token is not wrapped.
496 """
497 return None
498
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200499 @abstractmethod
500 def _parse_p_header(self, msg):
501 """Throw exception in case of error"""
502
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200503 def _get_cose_alg(self):
504 return self.cose_alg
505
506 def _get_method(self):
507 return self.method
508
509 def _get_signing_key(self):
510 return self.signing_key
511
512 def __init__(
513 self,
514 *, method,
515 cose_alg,
516 signing_key,
517 claims,
518 configuration=None,
519 necessity=AttestationClaim.MANDATORY):
520 self.method = method
521 self.cose_alg = cose_alg
522 self.signing_key=signing_key
523 self.config = configuration if configuration is not None else VerifierConfiguration()
524 self.seen_errors = False
525 self.claims = AttestTokenRootClaims(
526 verifier=self,
527 claims=claims,
528 is_list=False,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200529 necessity=necessity)
530
531 super().__init__(verifier=self, necessity=necessity)
532
Mate Toth-Pale305e552022-10-07 14:04:53 +0200533 def _sign_token(self, token):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200534 """Signs a token"""
535 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
536 return token
537 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
Mate Toth-Pale305e552022-10-07 14:04:53 +0200538 return self._sign_eat(token)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200539 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
Mate Toth-Pale305e552022-10-07 14:04:53 +0200540 return self._hmac_eat(token)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200541 err_msg = 'Unexpected method "{}"; must be one of: raw, sign, mac'
542 raise ValueError(err_msg.format(self.method))
543
Mate Toth-Pale305e552022-10-07 14:04:53 +0200544 def _sign_eat(self, token):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200545 p_header=self._get_p_header()
546 key=self._get_signing_key()
Thomas Fossatif4e1ca32024-08-16 16:01:31 +0000547 signed_msg = Sign1Message(payload=token, key=key, phdr=p_header)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200548 return signed_msg.encode()
549
550
Mate Toth-Pale305e552022-10-07 14:04:53 +0200551 def _hmac_eat(self, token):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200552 p_header=self._get_p_header()
553 key=self._get_signing_key()
Thomas Fossatif4e1ca32024-08-16 16:01:31 +0000554 hmac_msg = Mac0Message(payload=token, key=key, phdr=p_header)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200555 return hmac_msg.encode()
556
557
Mate Toth-Pale305e552022-10-07 14:04:53 +0200558 def _get_cose_sign1_payload(self, cose, *, verify_signature):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200559 msg = Sign1Message.decode(cose)
560 if verify_signature:
561 key = self._get_signing_key()
Mate Toth-Pale305e552022-10-07 14:04:53 +0200562 try:
563 self._parse_p_header(msg)
564 except Exception as exc:
565 self.error(f'Invalid Protected header: {exc}', exception=exc)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200566 msg.key = key
Thomas Fossatif4e1ca32024-08-16 16:01:31 +0000567 if not msg.verify_signature():
568 raise ValueError('Bad signature')
569 return msg.payload, msg.phdr
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200570
571
Mate Toth-Pale305e552022-10-07 14:04:53 +0200572 def _get_cose_mac0_payload(self, cose, *, verify_signature):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200573 msg = Mac0Message.decode(cose)
574 if verify_signature:
575 key = self._get_signing_key()
Mate Toth-Pale305e552022-10-07 14:04:53 +0200576 try:
577 self._parse_p_header(msg)
578 except Exception as exc:
579 self.error(f'Invalid Protected header: {exc}', exception=exc)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200580 msg.key = key
Thomas Fossatif4e1ca32024-08-16 16:01:31 +0000581 if not msg.verify_tag(alg=self._get_cose_alg()):
582 raise ValueError('Bad signature')
583 return msg.payload, msg.phdr
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200584
585
Mate Toth-Pale305e552022-10-07 14:04:53 +0200586 def _get_cose_payload(self, cose, *, verify_signature):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200587 """Return the payload of a COSE envelope"""
588 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
589 return self._get_cose_sign1_payload(
590 cose,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200591 verify_signature=verify_signature)
592 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
593 return self._get_cose_mac0_payload(
594 cose,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200595 verify_signature=verify_signature)
596 err_msg = f'Unexpected method "{self._get_method()}"; must be one of: sign, mac'
597 raise ValueError(err_msg)
598
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200599 def convert_map_to_token(
600 self,
601 token_encoder,
602 token_map,
Mate Toth-Pale305e552022-10-07 14:04:53 +0200603 *, name_as_key,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200604 parse_raw_value,
605 root=False):
606 with BytesIO() as b_io:
607 # Create a new encoder instance
608 encoder = CBOREncoder(b_io)
609
610 # Add tag if necessary
611 wrapping_tag = self._get_wrapping_tag()
612 if wrapping_tag is not None:
613 # TODO: this doesn't saves the string references used up to the
614 # point that this tag is added (see encode_semantic(...) in cbor2's
615 # encoder.py). This is not a problem as far the tokens don't use
616 # string references (which is the case for now).
617 encoder.encode_length(_CBOR_MAJOR_TYPE_SEMANTIC_TAG, wrapping_tag)
618
619 # Encode the token payload
620 self.claims.convert_map_to_token(
621 encoder,
622 token_map,
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200623 name_as_key=name_as_key,
624 parse_raw_value=parse_raw_value)
625
626 token = b_io.getvalue()
627
628 # Sign and pack in a COSE envelope if necessary
Mate Toth-Pale305e552022-10-07 14:04:53 +0200629 signed_token = self._sign_token(token)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200630
631 # Pack as a bstr if necessary
632 if root:
633 token_encoder.write(signed_token)
634 else:
635 token_encoder.encode_bytestring(signed_token)
636
Mate Toth-Pale305e552022-10-07 14:04:53 +0200637 def parse_token(self, *, token, lower_case_key):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200638 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
639 payload = token
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200640 protected_header = None
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200641 else:
Thomas Fossatif4e1ca32024-08-16 16:01:31 +0000642 verify_signature = False
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200643 try:
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200644 payload, protected_header = self._get_cose_payload(
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200645 token,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200646 # signature verification is done in the verify function
Thomas Fossatif4e1ca32024-08-16 16:01:31 +0000647 verify_signature=verify_signature)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200648 except Exception as exc:
Thomas Fossatif4e1ca32024-08-16 16:01:31 +0000649 msg = f'Parse (verify_signature={verify_signature}): Bad COSE: {exc}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200650 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200651
652 try:
653 raw_map = cbor2.loads(payload)
654 except Exception as exc:
655 msg = f'Invalid CBOR: {exc}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200656 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200657
Mate Toth-Pale589c452022-07-27 22:02:40 +0200658 if isinstance(raw_map, _cbor2.CBORTag):
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200659 raw_map_tag = raw_map.tag
660 raw_map = raw_map.value
661 else:
662 raw_map_tag = None
663
664 token_items = self.claims.parse_token(
665 token=raw_map,
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200666 lower_case_key=lower_case_key)
667
668 ret = TokenItem(value=token_items, claim_type=self)
669 ret.wrapping_tag = raw_map_tag
670 ret.token = token
Mate Toth-Palb21ae522022-09-01 12:02:21 +0200671 ret.protected_header = protected_header
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200672 return ret
673
674 def verify(self, token_item):
675 if self._get_method() != AttestationTokenVerifier.SIGN_METHOD_RAW:
Thomas Fossatif4e1ca32024-08-16 16:01:31 +0000676 verify_signature = self._get_signing_key() is not None
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200677 try:
678 self._get_cose_payload(
679 token_item.token,
Thomas Fossatif4e1ca32024-08-16 16:01:31 +0000680 verify_signature=verify_signature)
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200681 except Exception as exc:
Thomas Fossatif4e1ca32024-08-16 16:01:31 +0000682 msg = f'Verify (verify_signature={verify_signature}): Bad COSE: {exc}'
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200683 raise ValueError(msg) from exc
684
685 wrapping_tag = self._get_wrapping_tag()
686 if token_item.wrapping_tag is not None:
Mate Toth-Pale589c452022-07-27 22:02:40 +0200687 if wrapping_tag is None:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200688 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 +0200689 self.error(msg)
Mate Toth-Pale589c452022-07-27 22:02:40 +0200690 else:
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200691 if wrapping_tag != token_item.wrapping_tag:
692 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 +0200693 self.error(msg)
Mate Toth-Pale589c452022-07-27 22:02:40 +0200694 else:
695 if wrapping_tag is not None:
696 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 +0200697 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200698
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200699 token_item.value.verify()
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200700
Mate Toth-Palc7404e92022-07-15 11:11:13 +0200701 def get_token_map(self, token_item):
702 return self.claims.get_token_map(token_item.value)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200703
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200704 def error(self, message, *, exception=None):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200705 """Act on an error depending on the configuration of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200706 self.seen_errors = True
707 if self.config.keep_going:
708 logger.error(message)
709 else:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200710 if exception is None:
711 raise ValueError(message)
712 else:
713 raise ValueError(message) from Exception
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200714
715 def warning(self, message):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200716 """Print a warning with the logger of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200717 logger.warning(message)