blob: bd934414efca98bc0788837df21bc78bd249800a [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.
231 """
232
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200233 def __init__(self,
234 *, verifier,
235 claims,
236 is_list,
237 cross_claim_requirement_checker,
238 necessity=AttestationClaim.MANDATORY):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200239 """ Initialise a composite claim.
240
241 In case 'is_list' is False, the expected type of value is a dictionary,
242 containing the necessary claims determined by the 'claims' list.
243 In case 'is_list' is True, the expected type of value is a list,
244 containing a number of dictionaries, each one containing the necessary
245 claims determined by the 'claims' list.
246 """
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200247 super().__init__(verifier=verifier, necessity=necessity)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200248 self.is_list = is_list
249 self.claims = claims
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200250 self.cross_claim_requirement_checker = cross_claim_requirement_checker
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200251
252 def _get_contained_claims(self):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200253 claims = []
254 for claim, args in self.claims:
255 try:
256 claims.append(claim(**args))
257 except TypeError as exc:
258 raise TypeError(f"Failed to instantiate '{claim}' with args '{args}' in token " +
259 f"{type(self.verifier)}\nSee error in exception above.") from exc
260 return claims
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200261
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200262
263 def verify(self, value):
264 self.verify_count += 1
265
266 def _parse_token_dict(self, *, entry_number, token, verify, check_p_header, lower_case_key):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200267 ret = {}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200268
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200269 if verify:
270 self.verify(token)
271 if not self._check_type(self.get_claim_name(), token, dict):
272 return None
273 else:
274 if not isinstance(token, dict):
275 return token
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200276
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200277 claims = {val.get_claim_key(): val for val in self._get_contained_claims()}
278 for key, val in token.items():
279 if key not in claims.keys():
280 if verify and self.config.strict:
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200281 msg = 'Unexpected {} claim: {}'
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200282 self.verifier.error(msg.format(self.get_claim_name(), key))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200283 else:
284 continue
285 try:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200286 claim = claims[key]
287 name = claim.get_claim_name()
288 if lower_case_key:
289 name = name.lower()
290 ret[name] = claim.parse_token(
291 token=val,
292 verify=verify,
293 check_p_header=check_p_header,
294 lower_case_key=lower_case_key)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200295 except Exception:
296 if not self.config.keep_going:
297 raise
298
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200299 if verify:
300 self._check_claims_necessity(entry_number, claims)
301 if self.cross_claim_requirement_checker is not None:
302 self.cross_claim_requirement_checker(self.verifier, claims)
303
304 return ret
305
306 def _check_claims_necessity(self, entry_number, claims):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200307 for claim in claims.values():
308 if not claim.claim_found():
309 if claim.necessity==AttestationClaim.MANDATORY:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200310 msg = (f'Invalid IAT: missing MANDATORY claim "{claim.get_claim_name()}" '
311 f'from {self.get_claim_name()}')
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200312 if entry_number is not None:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200313 msg += f' at index {entry_number}'
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200314 self.verifier.error(msg)
315 elif claim.necessity==AttestationClaim.RECOMMENDED:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200316 msg = (f'Missing RECOMMENDED claim "{claim.get_claim_name()}" '
317 f'from {self.get_claim_name()}')
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200318 if entry_number is not None:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200319 msg += f' at index {entry_number}'
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200320 self.verifier.warning(msg)
321
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200322 def parse_token(self, *, token, verify, check_p_header, lower_case_key):
323 """This expects a raw token map as 'token'"""
324
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200325 if self.is_list:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200326 ret = []
327 if verify:
328 if not self._check_type(self.get_claim_name(), token, list):
329 return None
330 else:
331 if not isinstance(token, list):
332 return token
333 for entry_number, entry in enumerate(token):
334 ret.append(self._parse_token_dict(
335 entry_number=entry_number,
336 check_p_header=check_p_header,
337 token=entry,
338 verify=verify,
339 lower_case_key=lower_case_key))
340 return ret
341 return self._parse_token_dict(
342 entry_number=None,
343 check_p_header=check_p_header,
344 token=token,
345 verify=verify,
346 lower_case_key=lower_case_key)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200347
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200348
349 def _encode_dict(self, token_encoder, token_map, *, add_p_header, name_as_key, parse_raw_value):
350 token_encoder.encode_length(_CBOR_MAJOR_TYPE_MAP, len(token_map))
351 if name_as_key:
352 claims = {claim.get_claim_name().lower():
353 claim for claim in self._get_contained_claims()}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200354 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200355 claims = {claim.get_claim_key(): claim for claim in self._get_contained_claims()}
356 for key, val in token_map.items():
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200357 try:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200358 claim = claims[key]
359 key = claim.get_claim_key()
360 token_encoder.encode(key)
361 claim.convert_map_to_token(
362 token_encoder,
363 val,
364 add_p_header=add_p_header,
365 name_as_key=name_as_key,
366 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200367 except KeyError:
368 if self.config.strict:
369 if not self.config.keep_going:
370 raise
371 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200372 token_encoder.encode(key)
373 token_encoder.encode(val)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200374
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200375 def convert_map_to_token(
376 self,
377 token_encoder,
378 token_map,
379 *, add_p_header,
380 name_as_key,
381 parse_raw_value):
382 if self.is_list:
383 token_encoder.encode_length(_CBOR_MAJOR_TYPE_ARRAY, len(token_map))
384 for item in token_map:
385 self._encode_dict(
386 token_encoder,
387 item,
388 add_p_header=add_p_header,
389 name_as_key=name_as_key,
390 parse_raw_value=parse_raw_value)
391 else:
392 self._encode_dict(
393 token_encoder,
394 token_map,
395 add_p_header=add_p_header,
396 name_as_key=name_as_key,
397 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200398
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200399
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200400@dataclass
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200401class VerifierConfiguration:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200402 """A class storing the configuration of the verifier.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200403
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200404 At the moment this determines what should happen if a problem is found
405 during verification.
406 """
407 keep_going: bool = False
408 strict: bool = False
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200409
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200410class AttestTokenRootClaims(CompositeAttestClaim):
411 """A claim type that is used to represent the claims in a token.
412
413 It is instantiated by AttestationTokenVerifier, and shouldn't be used
414 outside this module."""
415 def get_claim_key(self=None):
416 return None
417
418 def get_claim_name(self=None):
419 return None
420
421# This class inherits from NonVerifiedClaim. The actual claims in the token are
422# checked by the AttestTokenRootClaims object owned by this verifier. The
423# verify() function of the AttestTokenRootClaims object is called during
424# traversing the claim tree.
425class AttestationTokenVerifier(NonVerifiedClaim):
426 """Abstract base class for attestation token verifiers"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200427
428 SIGN_METHOD_SIGN1 = "sign"
429 SIGN_METHOD_MAC0 = "mac"
430 SIGN_METHOD_RAW = "raw"
431
432 COSE_ALG_ES256="ES256"
433 COSE_ALG_ES384="ES384"
434 COSE_ALG_ES512="ES512"
435 COSE_ALG_HS256_64="HS256/64"
436 COSE_ALG_HS256="HS256"
437 COSE_ALG_HS384="HS384"
438 COSE_ALG_HS512="HS512"
439
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200440 @abstractmethod
441 def _get_p_header(self):
442 """Return the protected header for this Token
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200443
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200444 Return a dictionary if p_header should be present, and None if the token
445 doesn't defines a protected header.
446 """
447 raise NotImplementedError
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200448
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200449 @abstractmethod
450 def _get_wrapping_tag(self):
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200451 """The value of the tag that the token is wrapped in.
452
453 The function should return None if the token is not wrapped.
454 """
455 return None
456
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200457 @abstractmethod
458 def _parse_p_header(self, msg):
459 """Throw exception in case of error"""
460
461 @staticmethod
462 @abstractmethod
463 def check_cross_claim_requirements(verifier, claims):
464 """Throw exception in case of error"""
465
466 def _get_cose_alg(self):
467 return self.cose_alg
468
469 def _get_method(self):
470 return self.method
471
472 def _get_signing_key(self):
473 return self.signing_key
474
475 def __init__(
476 self,
477 *, method,
478 cose_alg,
479 signing_key,
480 claims,
481 configuration=None,
482 necessity=AttestationClaim.MANDATORY):
483 self.method = method
484 self.cose_alg = cose_alg
485 self.signing_key=signing_key
486 self.config = configuration if configuration is not None else VerifierConfiguration()
487 self.seen_errors = False
488 self.claims = AttestTokenRootClaims(
489 verifier=self,
490 claims=claims,
491 is_list=False,
492 cross_claim_requirement_checker=type(self).check_cross_claim_requirements,
493 necessity=necessity)
494
495 super().__init__(verifier=self, necessity=necessity)
496
497 def _sign_token(self, token, add_p_header):
498 """Signs a token"""
499 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
500 return token
501 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
502 return self._sign_eat(token, add_p_header)
503 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
504 return self._hmac_eat(token, add_p_header)
505 err_msg = 'Unexpected method "{}"; must be one of: raw, sign, mac'
506 raise ValueError(err_msg.format(self.method))
507
508 def _sign_eat(self, token, add_p_header):
509 protected_header = CoseAttrs()
510 p_header=self._get_p_header()
511 key=self._get_signing_key()
512 if add_p_header and p_header is not None and key:
513 protected_header.update(p_header)
514 signed_msg = Sign1Message(p_header=protected_header)
515 signed_msg.payload = token
516 if key:
517 signed_msg.key = key
518 signed_msg.signature = signed_msg.compute_signature(alg=self._get_cose_alg())
519 return signed_msg.encode()
520
521
522 def _hmac_eat(self, token, add_p_header):
523 protected_header = CoseAttrs()
524 p_header=self._get_p_header()
525 key=self._get_signing_key()
526 if add_p_header and p_header is not None and key:
527 protected_header.update(p_header)
528 hmac_msg = Mac0Message(payload=token, key=key, p_header=protected_header)
529 hmac_msg.compute_auth_tag(alg=self.cose_alg)
530 return hmac_msg.encode()
531
532
533 def _get_cose_sign1_payload(self, cose, *, check_p_header, verify_signature):
534 msg = Sign1Message.decode(cose)
535 if verify_signature:
536 key = self._get_signing_key()
537 if check_p_header:
538 self._parse_p_header(msg)
539 msg.key = key
540 msg.signature = msg.signers
541 try:
542 msg.verify_signature(alg=self._get_cose_alg())
543 except Exception as exc:
544 raise ValueError(f'Bad signature ({exc})') from exc
545 return msg.payload
546
547
548 def _get_cose_mac0_payload(self, cose, *, check_p_header, verify_signature):
549 msg = Mac0Message.decode(cose)
550 if verify_signature:
551 key = self._get_signing_key()
552 if check_p_header:
553 self._parse_p_header(msg)
554 msg.key = key
555 try:
556 msg.verify_auth_tag(alg=self._get_cose_alg())
557 except Exception as exc:
558 raise ValueError(f'Bad signature ({exc})') from exc
559 return msg.payload
560
561
562 def _get_cose_payload(self, cose, *, check_p_header, verify_signature):
563 """Return the payload of a COSE envelope"""
564 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
565 return self._get_cose_sign1_payload(
566 cose,
567 check_p_header=check_p_header,
568 verify_signature=verify_signature)
569 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
570 return self._get_cose_mac0_payload(
571 cose,
572 check_p_header=check_p_header,
573 verify_signature=verify_signature)
574 err_msg = f'Unexpected method "{self._get_method()}"; must be one of: sign, mac'
575 raise ValueError(err_msg)
576
577
578 def convert_map_to_token(
579 self,
580 token_encoder,
581 token_map,
582 *, add_p_header,
583 name_as_key,
584 parse_raw_value,
585 root=False):
586 with BytesIO() as b_io:
587 # Create a new encoder instance
588 encoder = CBOREncoder(b_io)
589
590 # Add tag if necessary
591 wrapping_tag = self._get_wrapping_tag()
592 if wrapping_tag is not None:
593 # TODO: this doesn't saves the string references used up to the
594 # point that this tag is added (see encode_semantic(...) in cbor2's
595 # encoder.py). This is not a problem as far the tokens don't use
596 # string references (which is the case for now).
597 encoder.encode_length(_CBOR_MAJOR_TYPE_SEMANTIC_TAG, wrapping_tag)
598
599 # Encode the token payload
600 self.claims.convert_map_to_token(
601 encoder,
602 token_map,
603 add_p_header=add_p_header,
604 name_as_key=name_as_key,
605 parse_raw_value=parse_raw_value)
606
607 token = b_io.getvalue()
608
609 # Sign and pack in a COSE envelope if necessary
610 signed_token = self._sign_token(token, add_p_header=add_p_header)
611
612 # Pack as a bstr if necessary
613 if root:
614 token_encoder.write(signed_token)
615 else:
616 token_encoder.encode_bytestring(signed_token)
617
618 def parse_token(self, *, token, verify, check_p_header, lower_case_key):
619 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
620 payload = token
621 else:
622 try:
623 payload = self._get_cose_payload(
624 token,
625 check_p_header=check_p_header,
626 verify_signature=(verify and self._get_signing_key() is not None))
627 except Exception as exc:
628 msg = f'Bad COSE: {exc}'
629 raise ValueError(msg) from exc
630
631 try:
632 raw_map = cbor2.loads(payload)
633 except Exception as exc:
634 msg = f'Invalid CBOR: {exc}'
635 raise ValueError(msg) from exc
636
637 wrapping_tag = self._get_wrapping_tag()
638 if wrapping_tag is not None:
639 if verify and wrapping_tag != raw_map.tag:
640 msg = 'Invalid token: token is wrapped in tag {} instead of {}'
641 raise ValueError(msg.format(raw_map.tag, wrapping_tag))
642 raw_map = raw_map.value
643
644 if verify:
645 self.verify(token)
646
647 return self.claims.parse_token(
648 token=raw_map,
649 check_p_header=check_p_header,
650 verify=verify,
651 lower_case_key=lower_case_key)
652
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200653 def error(self, message):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200654 """Act on an error depending on the configuration of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200655 self.seen_errors = True
656 if self.config.keep_going:
657 logger.error(message)
658 else:
659 raise ValueError(message)
660
661 def warning(self, message):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200662 """Print a warning with the logger of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200663 logger.warning(message)