blob: c0f8e5f7b66325f2732c40dfbab9ef4108fc1951 [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-Pald10a9142022-04-28 15:34:13 +020032class AttestationClaim(ABC):
33 """
34 This class represents a claim.
35
36 This class is abstract. A concrete claim have to be derived from this class,
37 and it have to implement all the abstract methods.
38
39 This class contains methods that are not abstract. These are here as a
40 default behavior, that a derived class might either keep, or override.
41
42 A token is built up as a hierarchy of claim classes. Although it is
43 important, that claim objects don't have a 'value' field. The actual parsed
44 token is stored in a map structure. It is possible to execute operations
45 on a token map, and the operations are defined by claim classes/objects.
46 Such operations are for example verifying a token.
47 """
48
Mate Toth-Palbb187d02022-04-26 16:01:51 +020049 MANDATORY = 0
50 RECOMMENDED = 1
51 OPTIONAL = 2
52
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020053 def __init__(self, *, verifier, necessity=MANDATORY):
Mate Toth-Palbb187d02022-04-26 16:01:51 +020054 self.config = verifier.config
55 self.verifier = verifier
56 self.necessity = necessity
57 self.verify_count = 0
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020058 self.cross_claim_requirement_checker = None
Mate Toth-Palbb187d02022-04-26 16:01:51 +020059
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020060 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +020061 # Abstract methods
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020062 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +020063
64 @abstractmethod
Mate Toth-Palbb187d02022-04-26 16:01:51 +020065 def verify(self, value):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020066 """Verify this claim
67
68 Throw an exception if the claim is not valid"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +020069 raise NotImplementedError
70
Mate Toth-Pald10a9142022-04-28 15:34:13 +020071 @abstractmethod
Mate Toth-Palbb187d02022-04-26 16:01:51 +020072 def get_claim_key(self=None):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020073 """Get the key of this claim
74
75 Returns the key of this claim. The implementation have to support
76 calling this method with or without an instance as well."""
Mate Toth-Palbb187d02022-04-26 16:01:51 +020077 raise NotImplementedError
78
Mate Toth-Pald10a9142022-04-28 15:34:13 +020079 @abstractmethod
Mate Toth-Palbb187d02022-04-26 16:01:51 +020080 def get_claim_name(self=None):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020081 """Get the name of this claim
82
83 Returns the name of this claim. The implementation have to support
84 calling this method with or without an instance as well."""
Mate Toth-Palbb187d02022-04-26 16:01:51 +020085 raise NotImplementedError
86
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020087 #
Mate Toth-Pald10a9142022-04-28 15:34:13 +020088 # Default methods that a derived class might override
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020089 #
Mate Toth-Palbb187d02022-04-26 16:01:51 +020090
91 def decode(self, value):
Mate Toth-Pald10a9142022-04-28 15:34:13 +020092 """
93 Decode the value of the claim if the value is an UTF-8 string
94 """
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020095 if type(self).is_utf_8():
Mate Toth-Palbb187d02022-04-26 16:01:51 +020096 try:
97 return value.decode()
Mate Toth-Palb9057ff2022-04-29 16:03:21 +020098 except UnicodeDecodeError as exc:
Mate Toth-Palbb187d02022-04-26 16:01:51 +020099 msg = 'Error decodeing value for "{}": {}'
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200100 self.verifier.error(msg.format(self.get_claim_name(), exc))
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200101 return str(value)[2:-1]
102 else: # not a UTF-8 value, i.e. a bytestring
103 return value
104
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200105 def claim_found(self):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200106 """Return true if verify was called on tis claim instance"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200107 return self.verify_count>0
108
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200109 @classmethod
110 def is_utf_8(cls):
111 """Returns whether the value of this claim should be UTF-8"""
112 return False
113
114 def convert_map_to_token(self,
115 token_encoder,
116 token_map,
117 *, add_p_header,
118 name_as_key,
119 parse_raw_value):
120 """Encode a map in cbor format using the 'token_encoder'"""
121 # pylint: disable=unused-argument
122 value = token_map
123 if parse_raw_value:
124 value = type(self).parse_raw(value)
125 return token_encoder.encode(value)
126
127 def parse_token(self, *, token, verify, check_p_header, lower_case_key):
128 """Parse a token into a map
129
130 This function is recursive for composite claims and for token verifiers.
131 A big difference is that the parameter token should be a map for claim
132 objects, and a 'bytes' object for verifiers. The entry point to this
133 function is calling the parse_token function of a verifier.
134
135 From some aspects it would be cleaner to have different functions for
136 this in verifiers and claims, but that would require to do a type check
137 in every recursive step to see which method to call. So instead the
138 method name is the same, and the 'token' parameter is interpreted
139 differently."""
140 # pylint: disable=unused-argument
141 if verify:
142 self.verify(token)
143
144 formatted = type(self).get_formatted_value(token)
145
146 # If the formatted value is still a bytestring then try to decode
147 if isinstance(formatted, bytes):
148 formatted = self.decode(formatted)
149 return formatted
150
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-Palbb187d02022-04-26 16:01:51 +0200216
217class NonVerifiedClaim(AttestationClaim):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200218 """An abstract claim type for which verify() always passes.
219
220 Can be used for claims for which no verification is implemented."""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200221 def verify(self, value):
222 self.verify_count += 1
223
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200224class CompositeAttestClaim(AttestationClaim):
225 """
226 This class represents composite claim.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200227
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200228 This class is still abstract, but can contain other claims. This means that
229 a value representing this claim is a dictionary. This claim contains further
230 claims which represent the possible key-value pairs in the value for this
231 claim.
Mate Toth-Pal530106f2022-05-03 15:29:49 +0200232
233 It is possible that there are requirement that the claims in this claim must
234 satisfy, but this can't be checked in the `verify` function of a claim.
235
236 For example the composite claim can contain a claim type `A`, and a claim
237 type `B`, exactly one of the two can be present.
238
239 In this case a method must be passed in the `cross_claim_requirement_checker`
240 parameter of the `__init__` function, that does this check.
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200241 """
242
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200243 def __init__(self,
244 *, verifier,
245 claims,
246 is_list,
247 cross_claim_requirement_checker,
248 necessity=AttestationClaim.MANDATORY):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200249 """ Initialise a composite claim.
250
251 In case 'is_list' is False, the expected type of value is a dictionary,
252 containing the necessary claims determined by the 'claims' list.
253 In case 'is_list' is True, the expected type of value is a list,
254 containing a number of dictionaries, each one containing the necessary
255 claims determined by the 'claims' list.
256 """
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200257 super().__init__(verifier=verifier, necessity=necessity)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200258 self.is_list = is_list
259 self.claims = claims
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200260 self.cross_claim_requirement_checker = cross_claim_requirement_checker
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200261
262 def _get_contained_claims(self):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200263 claims = []
264 for claim, args in self.claims:
265 try:
266 claims.append(claim(**args))
267 except TypeError as exc:
268 raise TypeError(f"Failed to instantiate '{claim}' with args '{args}' in token " +
269 f"{type(self.verifier)}\nSee error in exception above.") from exc
270 return claims
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200271
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200272
273 def verify(self, value):
Mate Toth-Pal530106f2022-05-03 15:29:49 +0200274 # No actual verification is done here. The `verify` function of the contained claims
275 # is called during traversing of the token tree.
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200276 self.verify_count += 1
277
278 def _parse_token_dict(self, *, entry_number, token, verify, check_p_header, lower_case_key):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200279 ret = {}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200280
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200281 if verify:
282 self.verify(token)
283 if not self._check_type(self.get_claim_name(), token, dict):
284 return None
285 else:
286 if not isinstance(token, dict):
287 return token
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200288
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200289 claims = {val.get_claim_key(): val for val in self._get_contained_claims()}
290 for key, val in token.items():
291 if key not in claims.keys():
292 if verify and self.config.strict:
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200293 msg = 'Unexpected {} claim: {}'
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200294 self.verifier.error(msg.format(self.get_claim_name(), key))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200295 else:
Mate Toth-Pal5ebca512022-03-24 16:45:51 +0100296 msg = 'Unexpected {} claim: {}, skipping.'
297 self.verifier.warning(msg.format(self.get_claim_name(), key))
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200298 continue
299 try:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200300 claim = claims[key]
301 name = claim.get_claim_name()
302 if lower_case_key:
303 name = name.lower()
304 ret[name] = claim.parse_token(
305 token=val,
306 verify=verify,
307 check_p_header=check_p_header,
308 lower_case_key=lower_case_key)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200309 except Exception:
310 if not self.config.keep_going:
311 raise
312
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200313 if verify:
314 self._check_claims_necessity(entry_number, claims)
315 if self.cross_claim_requirement_checker is not None:
316 self.cross_claim_requirement_checker(self.verifier, claims)
317
318 return ret
319
320 def _check_claims_necessity(self, entry_number, claims):
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200321 for claim in claims.values():
322 if not claim.claim_found():
323 if claim.necessity==AttestationClaim.MANDATORY:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200324 msg = (f'Invalid IAT: missing MANDATORY claim "{claim.get_claim_name()}" '
325 f'from {self.get_claim_name()}')
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200326 if entry_number is not None:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200327 msg += f' at index {entry_number}'
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200328 self.verifier.error(msg)
329 elif claim.necessity==AttestationClaim.RECOMMENDED:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200330 msg = (f'Missing RECOMMENDED claim "{claim.get_claim_name()}" '
331 f'from {self.get_claim_name()}')
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200332 if entry_number is not None:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200333 msg += f' at index {entry_number}'
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200334 self.verifier.warning(msg)
335
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200336 def parse_token(self, *, token, verify, check_p_header, lower_case_key):
337 """This expects a raw token map as 'token'"""
338
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200339 if self.is_list:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200340 ret = []
341 if verify:
342 if not self._check_type(self.get_claim_name(), token, list):
343 return None
344 else:
345 if not isinstance(token, list):
346 return token
347 for entry_number, entry in enumerate(token):
348 ret.append(self._parse_token_dict(
349 entry_number=entry_number,
350 check_p_header=check_p_header,
351 token=entry,
352 verify=verify,
353 lower_case_key=lower_case_key))
354 return ret
355 return self._parse_token_dict(
356 entry_number=None,
357 check_p_header=check_p_header,
358 token=token,
359 verify=verify,
360 lower_case_key=lower_case_key)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200361
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200362
363 def _encode_dict(self, token_encoder, token_map, *, add_p_header, name_as_key, parse_raw_value):
364 token_encoder.encode_length(_CBOR_MAJOR_TYPE_MAP, len(token_map))
365 if name_as_key:
366 claims = {claim.get_claim_name().lower():
367 claim for claim in self._get_contained_claims()}
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200368 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200369 claims = {claim.get_claim_key(): claim for claim in self._get_contained_claims()}
370 for key, val in token_map.items():
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200371 try:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200372 claim = claims[key]
373 key = claim.get_claim_key()
374 token_encoder.encode(key)
375 claim.convert_map_to_token(
376 token_encoder,
377 val,
378 add_p_header=add_p_header,
379 name_as_key=name_as_key,
380 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200381 except KeyError:
382 if self.config.strict:
383 if not self.config.keep_going:
384 raise
385 else:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200386 token_encoder.encode(key)
387 token_encoder.encode(val)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200388
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200389 def convert_map_to_token(
390 self,
391 token_encoder,
392 token_map,
393 *, add_p_header,
394 name_as_key,
395 parse_raw_value):
396 if self.is_list:
397 token_encoder.encode_length(_CBOR_MAJOR_TYPE_ARRAY, len(token_map))
398 for item in token_map:
399 self._encode_dict(
400 token_encoder,
401 item,
402 add_p_header=add_p_header,
403 name_as_key=name_as_key,
404 parse_raw_value=parse_raw_value)
405 else:
406 self._encode_dict(
407 token_encoder,
408 token_map,
409 add_p_header=add_p_header,
410 name_as_key=name_as_key,
411 parse_raw_value=parse_raw_value)
Mate Toth-Pald10a9142022-04-28 15:34:13 +0200412
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200413
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200414@dataclass
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200415class VerifierConfiguration:
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200416 """A class storing the configuration of the verifier.
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200417
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200418 At the moment this determines what should happen if a problem is found
419 during verification.
420 """
421 keep_going: bool = False
422 strict: bool = False
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200423
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200424class AttestTokenRootClaims(CompositeAttestClaim):
425 """A claim type that is used to represent the claims in a token.
426
427 It is instantiated by AttestationTokenVerifier, and shouldn't be used
428 outside this module."""
429 def get_claim_key(self=None):
430 return None
431
432 def get_claim_name(self=None):
Mate Toth-Pal5ebca512022-03-24 16:45:51 +0100433 return "TOKEN_CLAIM"
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200434
435# This class inherits from NonVerifiedClaim. The actual claims in the token are
436# checked by the AttestTokenRootClaims object owned by this verifier. The
437# verify() function of the AttestTokenRootClaims object is called during
438# traversing the claim tree.
439class AttestationTokenVerifier(NonVerifiedClaim):
440 """Abstract base class for attestation token verifiers"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200441
442 SIGN_METHOD_SIGN1 = "sign"
443 SIGN_METHOD_MAC0 = "mac"
444 SIGN_METHOD_RAW = "raw"
445
446 COSE_ALG_ES256="ES256"
447 COSE_ALG_ES384="ES384"
448 COSE_ALG_ES512="ES512"
449 COSE_ALG_HS256_64="HS256/64"
450 COSE_ALG_HS256="HS256"
451 COSE_ALG_HS384="HS384"
452 COSE_ALG_HS512="HS512"
453
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200454 @abstractmethod
455 def _get_p_header(self):
456 """Return the protected header for this Token
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200457
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200458 Return a dictionary if p_header should be present, and None if the token
459 doesn't defines a protected header.
460 """
461 raise NotImplementedError
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200462
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200463 @abstractmethod
464 def _get_wrapping_tag(self):
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200465 """The value of the tag that the token is wrapped in.
466
467 The function should return None if the token is not wrapped.
468 """
469 return None
470
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200471 @abstractmethod
472 def _parse_p_header(self, msg):
473 """Throw exception in case of error"""
474
475 @staticmethod
476 @abstractmethod
477 def check_cross_claim_requirements(verifier, claims):
478 """Throw exception in case of error"""
479
480 def _get_cose_alg(self):
481 return self.cose_alg
482
483 def _get_method(self):
484 return self.method
485
486 def _get_signing_key(self):
487 return self.signing_key
488
489 def __init__(
490 self,
491 *, method,
492 cose_alg,
493 signing_key,
494 claims,
495 configuration=None,
496 necessity=AttestationClaim.MANDATORY):
497 self.method = method
498 self.cose_alg = cose_alg
499 self.signing_key=signing_key
500 self.config = configuration if configuration is not None else VerifierConfiguration()
501 self.seen_errors = False
502 self.claims = AttestTokenRootClaims(
503 verifier=self,
504 claims=claims,
505 is_list=False,
506 cross_claim_requirement_checker=type(self).check_cross_claim_requirements,
507 necessity=necessity)
508
509 super().__init__(verifier=self, necessity=necessity)
510
511 def _sign_token(self, token, add_p_header):
512 """Signs a token"""
513 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
514 return token
515 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
516 return self._sign_eat(token, add_p_header)
517 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
518 return self._hmac_eat(token, add_p_header)
519 err_msg = 'Unexpected method "{}"; must be one of: raw, sign, mac'
520 raise ValueError(err_msg.format(self.method))
521
522 def _sign_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 signed_msg = Sign1Message(p_header=protected_header)
529 signed_msg.payload = token
530 if key:
531 signed_msg.key = key
532 signed_msg.signature = signed_msg.compute_signature(alg=self._get_cose_alg())
533 return signed_msg.encode()
534
535
536 def _hmac_eat(self, token, add_p_header):
537 protected_header = CoseAttrs()
538 p_header=self._get_p_header()
539 key=self._get_signing_key()
540 if add_p_header and p_header is not None and key:
541 protected_header.update(p_header)
542 hmac_msg = Mac0Message(payload=token, key=key, p_header=protected_header)
543 hmac_msg.compute_auth_tag(alg=self.cose_alg)
544 return hmac_msg.encode()
545
546
547 def _get_cose_sign1_payload(self, cose, *, check_p_header, verify_signature):
548 msg = Sign1Message.decode(cose)
549 if verify_signature:
550 key = self._get_signing_key()
551 if check_p_header:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200552 try:
553 self._parse_p_header(msg)
554 except Exception as exc:
555 self.error(f'Invalid Protected header: {exc}', exception=exc)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200556 msg.key = key
557 msg.signature = msg.signers
558 try:
559 msg.verify_signature(alg=self._get_cose_alg())
560 except Exception as exc:
561 raise ValueError(f'Bad signature ({exc})') from exc
562 return msg.payload
563
564
565 def _get_cose_mac0_payload(self, cose, *, check_p_header, verify_signature):
566 msg = Mac0Message.decode(cose)
567 if verify_signature:
568 key = self._get_signing_key()
569 if check_p_header:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200570 try:
571 self._parse_p_header(msg)
572 except Exception as exc:
573 self.error(f'Invalid Protected header: {exc}', exception=exc)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200574 msg.key = key
575 try:
576 msg.verify_auth_tag(alg=self._get_cose_alg())
577 except Exception as exc:
578 raise ValueError(f'Bad signature ({exc})') from exc
579 return msg.payload
580
581
582 def _get_cose_payload(self, cose, *, check_p_header, verify_signature):
583 """Return the payload of a COSE envelope"""
584 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
585 return self._get_cose_sign1_payload(
586 cose,
587 check_p_header=check_p_header,
588 verify_signature=verify_signature)
589 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_MAC0:
590 return self._get_cose_mac0_payload(
591 cose,
592 check_p_header=check_p_header,
593 verify_signature=verify_signature)
594 err_msg = f'Unexpected method "{self._get_method()}"; must be one of: sign, mac'
595 raise ValueError(err_msg)
596
597
598 def convert_map_to_token(
599 self,
600 token_encoder,
601 token_map,
602 *, add_p_header,
603 name_as_key,
604 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,
623 add_p_header=add_p_header,
624 name_as_key=name_as_key,
625 parse_raw_value=parse_raw_value)
626
627 token = b_io.getvalue()
628
629 # Sign and pack in a COSE envelope if necessary
630 signed_token = self._sign_token(token, add_p_header=add_p_header)
631
632 # Pack as a bstr if necessary
633 if root:
634 token_encoder.write(signed_token)
635 else:
636 token_encoder.encode_bytestring(signed_token)
637
638 def parse_token(self, *, token, verify, check_p_header, lower_case_key):
639 if self._get_method() == AttestationTokenVerifier.SIGN_METHOD_RAW:
640 payload = token
641 else:
642 try:
643 payload = self._get_cose_payload(
644 token,
645 check_p_header=check_p_header,
646 verify_signature=(verify and self._get_signing_key() is not None))
647 except Exception as exc:
648 msg = f'Bad COSE: {exc}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200649 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200650
651 try:
652 raw_map = cbor2.loads(payload)
653 except Exception as exc:
654 msg = f'Invalid CBOR: {exc}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200655 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200656
657 wrapping_tag = self._get_wrapping_tag()
Mate Toth-Pale589c452022-07-27 22:02:40 +0200658
659 if isinstance(raw_map, _cbor2.CBORTag):
660 if wrapping_tag is None:
661 msg = f'Invalid token: Unexpected tag (0x{raw_map.tag:x}) in token {self.get_claim_name()}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200662 self.error(msg)
Mate Toth-Pale589c452022-07-27 22:02:40 +0200663 else:
664 if wrapping_tag != raw_map.tag:
665 msg = f'Invalid token: token {self.get_claim_name()} is wrapped in tag 0x{raw_map.tag:x} instead of 0x{wrapping_tag:x}'
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200666 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200667 raw_map = raw_map.value
Mate Toth-Pale589c452022-07-27 22:02:40 +0200668 else:
669 if wrapping_tag is not None:
670 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 +0200671 self.error(msg)
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200672
673 if verify:
674 self.verify(token)
675
676 return self.claims.parse_token(
677 token=raw_map,
678 check_p_header=check_p_header,
679 verify=verify,
680 lower_case_key=lower_case_key)
681
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200682 def error(self, message, *, exception=None):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200683 """Act on an error depending on the configuration of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200684 self.seen_errors = True
685 if self.config.keep_going:
686 logger.error(message)
687 else:
Mate Toth-Pal138637a2022-07-28 10:57:06 +0200688 if exception is None:
689 raise ValueError(message)
690 else:
691 raise ValueError(message) from Exception
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200692
693 def warning(self, message):
Mate Toth-Palb9057ff2022-04-29 16:03:21 +0200694 """Print a warning with the logger of this verifier"""
Mate Toth-Palbb187d02022-04-26 16:01:51 +0200695 logger.warning(message)