blob: 92b303a3d68b4cd2b3a0fa3baab0e4f159ebf1cf [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-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-Pald10a9142022-04-28 15:34:13 +020031class AttestationClaim(ABC):
32 """
33 This class represents a claim.
34
35 This class is abstract. A concrete claim have to be derived from this class,
36 and it have to implement all the abstract methods.
37
38 This class contains methods that are not abstract. These are here as a
39 default behavior, that a derived class might either keep, or override.
40
41 A token is built up as a hierarchy of claim classes. Although it is
42 important, that claim objects don't have a 'value' field. The actual parsed
43 token is stored in a map structure. It is possible to execute operations
44 on a token map, and the operations are defined by claim classes/objects.
45 Such operations are for example verifying a token.
46 """
47
Mate Toth-Palbb187d02022-04-26 16:01:51 +020048 MANDATORY = 0
49 RECOMMENDED = 1
50 OPTIONAL = 2
51
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020052 def __init__(self, *, verifier, necessity=MANDATORY):
Mate Toth-Palbb187d02022-04-26 16:01:51 +020053 self.config = verifier.config
54 self.verifier = verifier
55 self.necessity = necessity
56 self.verify_count = 0
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020057 self.cross_claim_requirement_checker = None
Mate Toth-Palbb187d02022-04-26 16:01:51 +020058
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020059 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +020060 # Abstract methods
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020061 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +020062
63 @abstractmethod
Mate Toth-Palbb187d02022-04-26 16:01:51 +020064 def verify(self, value):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020065 """Verify this claim
66
67 Throw an exception if the claim is not valid"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +020068 raise NotImplementedError
69
Mate Toth-Pald10a9142022-04-28 15:34:13 +020070 @abstractmethod
Mate Toth-Palbb187d02022-04-26 16:01:51 +020071 def get_claim_key(self=None):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020072 """Get the key of this claim
73
74 Returns the key of this claim. The implementation have to support
75 calling this method with or without an instance as well."""
Mate Toth-Palbb187d02022-04-26 16:01:51 +020076 raise NotImplementedError
77
Mate Toth-Pald10a9142022-04-28 15:34:13 +020078 @abstractmethod
Mate Toth-Palbb187d02022-04-26 16:01:51 +020079 def get_claim_name(self=None):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020080 """Get the name of this claim
81
82 Returns the name of this claim. The implementation have to support
83 calling this method with or without an instance as well."""
Mate Toth-Palbb187d02022-04-26 16:01:51 +020084 raise NotImplementedError
85
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020086 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +020087 # Default methods that a derived class might override
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020088 #
Mate Toth-Palbb187d02022-04-26 16:01:51 +020089
90 def decode(self, value):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020091 """
92 Decode the value of the claim if the value is an UTF-8 string
93 """
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020094 if type(self).is_utf_8():
Mate Toth-Palbb187d02022-04-26 16:01:51 +020095 try:
96 return value.decode()
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020097 except UnicodeDecodeError as exc:
Mate Toth-Palbb187d02022-04-26 16:01:51 +020098 msg = 'Error decodeing value for "{}": {}'
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020099 self.verifier.error(msg.format(self.get_claim_name(), exc))
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200100 return str(value)[2:-1]
101 else: # not a UTF-8 value, i.e. a bytestring
102 return value
103
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200104 def claim_found(self):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200105 """Return true if verify was called on tis claim instance"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200106 return self.verify_count>0
107
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200108 @classmethod
109 def is_utf_8(cls):
110 """Returns whether the value of this claim should be UTF-8"""
111 return False
112
113 def convert_map_to_token(self,
114 token_encoder,
115 token_map,
116 *, add_p_header,
117 name_as_key,
118 parse_raw_value):
119 """Encode a map in cbor format using the 'token_encoder'"""
120 # pylint: disable=unused-argument
121 value = token_map
122 if parse_raw_value:
123 value = type(self).parse_raw(value)
124 return token_encoder.encode(value)
125
126 def parse_token(self, *, token, verify, check_p_header, lower_case_key):
127 """Parse a token into a map
128
129 This function is recursive for composite claims and for token verifiers.
130 A big difference is that the parameter token should be a map for claim
131 objects, and a 'bytes' object for verifiers. The entry point to this
132 function is calling the parse_token function of a verifier.
133
134 From some aspects it would be cleaner to have different functions for
135 this in verifiers and claims, but that would require to do a type check
136 in every recursive step to see which method to call. So instead the
137 method name is the same, and the 'token' parameter is interpreted
138 differently."""
139 # pylint: disable=unused-argument
140 if verify:
141 self.verify(token)
142
143 formatted = type(self).get_formatted_value(token)
144
145 # If the formatted value is still a bytestring then try to decode
146 if isinstance(formatted, bytes):
147 formatted = self.decode(formatted)
148 return formatted
149
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-Palbb187d02022-04-26 16:01:51 +0200215
216class NonVerifiedClaim(AttestationClaim):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200217 """An abstract claim type for which verify() always passes.
218
219 Can be used for claims for which no verification is implemented."""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200220 def verify(self, value):
221 self.verify_count += 1
222
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200223class CompositeAttestClaim(AttestationClaim):
224 """
225 This class represents composite claim.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200226
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200227 This class is still abstract, but can contain other claims. This means that
228 a value representing this claim is a dictionary. This claim contains further
229 claims which represent the possible key-value pairs in the value for this
230 claim.
Mate Toth-Pal530106f2022-05-03 15:29:49 +0200231
232 It is possible that there are requirement that the claims in this claim must
233 satisfy, but this can't be checked in the `verify` function of a claim.
234
235 For example the composite claim can contain a claim type `A`, and a claim
236 type `B`, exactly one of the two can be present.
237
238 In this case a method must be passed in the `cross_claim_requirement_checker`
239 parameter of the `__init__` function, that does this check.
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200240 """
241
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200242 def __init__(self,
243 *, verifier,
244 claims,
245 is_list,
246 cross_claim_requirement_checker,
247 necessity=AttestationClaim.MANDATORY):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200248 """ Initialise a composite claim.
249
250 In case 'is_list' is False, the expected type of value is a dictionary,
251 containing the necessary claims determined by the 'claims' list.
252 In case 'is_list' is True, the expected type of value is a list,
253 containing a number of dictionaries, each one containing the necessary
254 claims determined by the 'claims' list.
255 """
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200256 super().__init__(verifier=verifier, necessity=necessity)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200257 self.is_list = is_list
258 self.claims = claims
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200259 self.cross_claim_requirement_checker = cross_claim_requirement_checker
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200260
261 def _get_contained_claims(self):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200262 claims = []
263 for claim, args in self.claims:
264 try:
265 claims.append(claim(**args))
266 except TypeError as exc:
267 raise TypeError(f"Failed to instantiate '{claim}' with args '{args}' in token " +
268 f"{type(self.verifier)}\nSee error in exception above.") from exc
269 return claims
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200270
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200271
272 def verify(self, value):
Mate Toth-Pal530106f2022-05-03 15:29:49 +0200273 # No actual verification is done here. The `verify` function of the contained claims
274 # is called during traversing of the token tree.
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200275 self.verify_count += 1
276
277 def _parse_token_dict(self, *, entry_number, token, verify, check_p_header, lower_case_key):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200278 ret = {}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200279
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200280 if verify:
281 self.verify(token)
282 if not self._check_type(self.get_claim_name(), token, dict):
283 return None
284 else:
285 if not isinstance(token, dict):
286 return token
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200287
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200288 claims = {val.get_claim_key(): val for val in self._get_contained_claims()}
289 for key, val in token.items():
290 if key not in claims.keys():
291 if verify and self.config.strict:
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200292 msg = 'Unexpected {} claim: {}'
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200293 self.verifier.error(msg.format(self.get_claim_name(), key))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200294 else:
Mate Toth-Pal5ebca512022-03-24 16:45:51 +0100295 msg = 'Unexpected {} claim: {}, skipping.'
296 self.verifier.warning(msg.format(self.get_claim_name(), key))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200297 continue
298 try:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200299 claim = claims[key]
300 name = claim.get_claim_name()
301 if lower_case_key:
302 name = name.lower()
303 ret[name] = claim.parse_token(
304 token=val,
305 verify=verify,
306 check_p_header=check_p_header,
307 lower_case_key=lower_case_key)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200308 except Exception:
309 if not self.config.keep_going:
310 raise
311
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200312 if verify:
313 self._check_claims_necessity(entry_number, claims)
314 if self.cross_claim_requirement_checker is not None:
315 self.cross_claim_requirement_checker(self.verifier, claims)
316
317 return ret
318
319 def _check_claims_necessity(self, entry_number, claims):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200320 for claim in claims.values():
321 if not claim.claim_found():
322 if claim.necessity==AttestationClaim.MANDATORY:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200323 msg = (f'Invalid IAT: missing MANDATORY claim "{claim.get_claim_name()}" '
324 f'from {self.get_claim_name()}')
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200325 if entry_number is not None:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200326 msg += f' at index {entry_number}'
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200327 self.verifier.error(msg)
328 elif claim.necessity==AttestationClaim.RECOMMENDED:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200329 msg = (f'Missing RECOMMENDED claim "{claim.get_claim_name()}" '
330 f'from {self.get_claim_name()}')
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200331 if entry_number is not None:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200332 msg += f' at index {entry_number}'
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200333 self.verifier.warning(msg)
334
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200335 def parse_token(self, *, token, verify, check_p_header, lower_case_key):
336 """This expects a raw token map as 'token'"""
337
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200338 if self.is_list:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200339 ret = []
340 if verify:
341 if not self._check_type(self.get_claim_name(), token, list):
342 return None
343 else:
344 if not isinstance(token, list):
345 return token
346 for entry_number, entry in enumerate(token):
347 ret.append(self._parse_token_dict(
348 entry_number=entry_number,
349 check_p_header=check_p_header,
350 token=entry,
351 verify=verify,
352 lower_case_key=lower_case_key))
353 return ret
354 return self._parse_token_dict(
355 entry_number=None,
356 check_p_header=check_p_header,
357 token=token,
358 verify=verify,
359 lower_case_key=lower_case_key)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200360
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200361
362 def _encode_dict(self, token_encoder, token_map, *, add_p_header, name_as_key, parse_raw_value):
363 token_encoder.encode_length(_CBOR_MAJOR_TYPE_MAP, len(token_map))
364 if name_as_key:
365 claims = {claim.get_claim_name().lower():
366 claim for claim in self._get_contained_claims()}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200367 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200368 claims = {claim.get_claim_key(): claim for claim in self._get_contained_claims()}
369 for key, val in token_map.items():
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200370 try:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200371 claim = claims[key]
372 key = claim.get_claim_key()
373 token_encoder.encode(key)
374 claim.convert_map_to_token(
375 token_encoder,
376 val,
377 add_p_header=add_p_header,
378 name_as_key=name_as_key,
379 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200380 except KeyError:
381 if self.config.strict:
382 if not self.config.keep_going:
383 raise
384 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200385 token_encoder.encode(key)
386 token_encoder.encode(val)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200387
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200388 def convert_map_to_token(
389 self,
390 token_encoder,
391 token_map,
392 *, add_p_header,
393 name_as_key,
394 parse_raw_value):
395 if self.is_list:
396 token_encoder.encode_length(_CBOR_MAJOR_TYPE_ARRAY, len(token_map))
397 for item in token_map:
398 self._encode_dict(
399 token_encoder,
400 item,
401 add_p_header=add_p_header,
402 name_as_key=name_as_key,
403 parse_raw_value=parse_raw_value)
404 else:
405 self._encode_dict(
406 token_encoder,
407 token_map,
408 add_p_header=add_p_header,
409 name_as_key=name_as_key,
410 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200411
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200412
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200413@dataclass
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200414class VerifierConfiguration:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200415 """A class storing the configuration of the verifier.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200416
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200417 At the moment this determines what should happen if a problem is found
418 during verification.
419 """
420 keep_going: bool = False
421 strict: bool = False
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200422
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200423class AttestTokenRootClaims(CompositeAttestClaim):
424 """A claim type that is used to represent the claims in a token.
425
426 It is instantiated by AttestationTokenVerifier, and shouldn't be used
427 outside this module."""
428 def get_claim_key(self=None):
429 return None
430
431 def get_claim_name(self=None):
Mate Toth-Pal5ebca512022-03-24 16:45:51 +0100432 return "TOKEN_CLAIM"
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200433
434# This class inherits from NonVerifiedClaim. The actual claims in the token are
435# checked by the AttestTokenRootClaims object owned by this verifier. The
436# verify() function of the AttestTokenRootClaims object is called during
437# traversing the claim tree.
438class AttestationTokenVerifier(NonVerifiedClaim):
439 """Abstract base class for attestation token verifiers"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200440
441 SIGN_METHOD_SIGN1 = "sign"
442 SIGN_METHOD_MAC0 = "mac"
443 SIGN_METHOD_RAW = "raw"
444
445 COSE_ALG_ES256="ES256"
446 COSE_ALG_ES384="ES384"
447 COSE_ALG_ES512="ES512"
448 COSE_ALG_HS256_64="HS256/64"
449 COSE_ALG_HS256="HS256"
450 COSE_ALG_HS384="HS384"
451 COSE_ALG_HS512="HS512"
452
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200453 @abstractmethod
454 def _get_p_header(self):
455 """Return the protected header for this Token
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200456
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200457 Return a dictionary if p_header should be present, and None if the token
458 doesn't defines a protected header.
459 """
460 raise NotImplementedError
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200461
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200462 @abstractmethod
463 def _get_wrapping_tag(self):
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200464 """The value of the tag that the token is wrapped in.
465
466 The function should return None if the token is not wrapped.
467 """
468 return None
469
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200470 @abstractmethod
471 def _parse_p_header(self, msg):
472 """Throw exception in case of error"""
473
474 @staticmethod
475 @abstractmethod
476 def check_cross_claim_requirements(verifier, claims):
477 """Throw exception in case of error"""
478
479 def _get_cose_alg(self):
480 return self.cose_alg
481
482 def _get_method(self):
483 return self.method
484
485 def _get_signing_key(self):
486 return self.signing_key
487
488 def __init__(
489 self,
490 *, method,
491 cose_alg,
492 signing_key,
493 claims,
494 configuration=None,
495 necessity=AttestationClaim.MANDATORY):
496 self.method = method
497 self.cose_alg = cose_alg
498 self.signing_key=signing_key
499 self.config = configuration if configuration is not None else VerifierConfiguration()
500 self.seen_errors = False
501 self.claims = AttestTokenRootClaims(
502 verifier=self,
503 claims=claims,
504 is_list=False,
505 cross_claim_requirement_checker=type(self).check_cross_claim_requirements,
506 necessity=necessity)
507
508 super().__init__(verifier=self, necessity=necessity)
509
510 def _sign_token(self, token, add_p_header):
511 """Signs a token"""
512 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
513 return token
514 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
515 return self._sign_eat(token, add_p_header)
516 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
517 return self._hmac_eat(token, add_p_header)
518 err_msg = 'Unexpected method "{}"; must be one of: raw, sign, mac'
519 raise ValueError(err_msg.format(self.method))
520
521 def _sign_eat(self, token, add_p_header):
522 protected_header = CoseAttrs()
523 p_header=self._get_p_header()
524 key=self._get_signing_key()
525 if add_p_header and p_header is not None and key:
526 protected_header.update(p_header)
527 signed_msg = Sign1Message(p_header=protected_header)
528 signed_msg.payload = token
529 if key:
530 signed_msg.key = key
531 signed_msg.signature = signed_msg.compute_signature(alg=self._get_cose_alg())
532 return signed_msg.encode()
533
534
535 def _hmac_eat(self, token, add_p_header):
536 protected_header = CoseAttrs()
537 p_header=self._get_p_header()
538 key=self._get_signing_key()
539 if add_p_header and p_header is not None and key:
540 protected_header.update(p_header)
541 hmac_msg = Mac0Message(payload=token, key=key, p_header=protected_header)
542 hmac_msg.compute_auth_tag(alg=self.cose_alg)
543 return hmac_msg.encode()
544
545
546 def _get_cose_sign1_payload(self, cose, *, check_p_header, verify_signature):
547 msg = Sign1Message.decode(cose)
548 if verify_signature:
549 key = self._get_signing_key()
550 if check_p_header:
551 self._parse_p_header(msg)
552 msg.key = key
553 msg.signature = msg.signers
554 try:
555 msg.verify_signature(alg=self._get_cose_alg())
556 except Exception as exc:
557 raise ValueError(f'Bad signature ({exc})') from exc
558 return msg.payload
559
560
561 def _get_cose_mac0_payload(self, cose, *, check_p_header, verify_signature):
562 msg = Mac0Message.decode(cose)
563 if verify_signature:
564 key = self._get_signing_key()
565 if check_p_header:
566 self._parse_p_header(msg)
567 msg.key = key
568 try:
569 msg.verify_auth_tag(alg=self._get_cose_alg())
570 except Exception as exc:
571 raise ValueError(f'Bad signature ({exc})') from exc
572 return msg.payload
573
574
575 def _get_cose_payload(self, cose, *, check_p_header, verify_signature):
576 """Return the payload of a COSE envelope"""
577 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
578 return self._get_cose_sign1_payload(
579 cose,
580 check_p_header=check_p_header,
581 verify_signature=verify_signature)
582 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
583 return self._get_cose_mac0_payload(
584 cose,
585 check_p_header=check_p_header,
586 verify_signature=verify_signature)
587 err_msg = f'Unexpected method "{self._get_method()}"; must be one of: sign, mac'
588 raise ValueError(err_msg)
589
590
591 def convert_map_to_token(
592 self,
593 token_encoder,
594 token_map,
595 *, add_p_header,
596 name_as_key,
597 parse_raw_value,
598 root=False):
599 with BytesIO() as b_io:
600 # Create a new encoder instance
601 encoder = CBOREncoder(b_io)
602
603 # Add tag if necessary
604 wrapping_tag = self._get_wrapping_tag()
605 if wrapping_tag is not None:
606 # TODO: this doesn't saves the string references used up to the
607 # point that this tag is added (see encode_semantic(...) in cbor2's
608 # encoder.py). This is not a problem as far the tokens don't use
609 # string references (which is the case for now).
610 encoder.encode_length(_CBOR_MAJOR_TYPE_SEMANTIC_TAG, wrapping_tag)
611
612 # Encode the token payload
613 self.claims.convert_map_to_token(
614 encoder,
615 token_map,
616 add_p_header=add_p_header,
617 name_as_key=name_as_key,
618 parse_raw_value=parse_raw_value)
619
620 token = b_io.getvalue()
621
622 # Sign and pack in a COSE envelope if necessary
623 signed_token = self._sign_token(token, add_p_header=add_p_header)
624
625 # Pack as a bstr if necessary
626 if root:
627 token_encoder.write(signed_token)
628 else:
629 token_encoder.encode_bytestring(signed_token)
630
631 def parse_token(self, *, token, verify, check_p_header, lower_case_key):
632 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
633 payload = token
634 else:
635 try:
636 payload = self._get_cose_payload(
637 token,
638 check_p_header=check_p_header,
639 verify_signature=(verify and self._get_signing_key() is not None))
640 except Exception as exc:
641 msg = f'Bad COSE: {exc}'
642 raise ValueError(msg) from exc
643
644 try:
645 raw_map = cbor2.loads(payload)
646 except Exception as exc:
647 msg = f'Invalid CBOR: {exc}'
648 raise ValueError(msg) from exc
649
650 wrapping_tag = self._get_wrapping_tag()
651 if wrapping_tag is not None:
652 if verify and wrapping_tag != raw_map.tag:
653 msg = 'Invalid token: token is wrapped in tag {} instead of {}'
654 raise ValueError(msg.format(raw_map.tag, wrapping_tag))
655 raw_map = raw_map.value
656
657 if verify:
658 self.verify(token)
659
660 return self.claims.parse_token(
661 token=raw_map,
662 check_p_header=check_p_header,
663 verify=verify,
664 lower_case_key=lower_case_key)
665
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200666 def error(self, message):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200667 """Act on an error depending on the configuration of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200668 self.seen_errors = True
669 if self.config.keep_going:
670 logger.error(message)
671 else:
672 raise ValueError(message)
673
674 def warning(self, message):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200675 """Print a warning with the logger of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200676 logger.warning(message)