From 0b745fe506cf262d884c0f0fe97962929e669059 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Tue, 22 Oct 2024 18:12:55 +0200 Subject: [PATCH 01/44] fix: variable name for more redability --- pyeudiw/jwk/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyeudiw/jwk/__init__.py b/pyeudiw/jwk/__init__.py index c5ec9030..91ed1fe0 100644 --- a/pyeudiw/jwk/__init__.py +++ b/pyeudiw/jwk/__init__.py @@ -173,8 +173,8 @@ def find_jwk_by_kid(kid: str, jwks: list[dict], as_dict: bool = True) -> dict | if not kid: raise InvalidKid("Kid cannot be empty") for jwk in jwks: - valid_jwk = jwk.get("kid", None) - if valid_jwk and kid == valid_jwk: + jwk_kid = jwk.get("kid", None) + if jwk_kid and kid == jwk_kid: return jwk if as_dict else JWK(jwk) raise KidNotFoundError(f"Key with Kid {kid} not found") From 5f63d42c94436c94f90ff73e2451428ec843db75 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Tue, 22 Oct 2024 18:29:16 +0200 Subject: [PATCH 02/44] fix: use already existent functions for unsafe parse --- pyeudiw/jwt/exceptions.py | 3 +++ pyeudiw/jwt/parse.py | 15 +++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/pyeudiw/jwt/exceptions.py b/pyeudiw/jwt/exceptions.py index 8005e22c..cb4c306f 100644 --- a/pyeudiw/jwt/exceptions.py +++ b/pyeudiw/jwt/exceptions.py @@ -12,3 +12,6 @@ class JWSVerificationError(Exception): class JWEEncryptionError(Exception): pass + +class JWTDecodeError(Exception): + pass \ No newline at end of file diff --git a/pyeudiw/jwt/parse.py b/pyeudiw/jwt/parse.py index a27ecdd3..5a4e8c11 100644 --- a/pyeudiw/jwt/parse.py +++ b/pyeudiw/jwt/parse.py @@ -1,9 +1,9 @@ from dataclasses import dataclass -from jwcrypto.common import base64url_decode, json_decode from pyeudiw.federation.trust_chain.parse import get_public_key_from_trust_chain from pyeudiw.jwk import JWK from pyeudiw.jwt.utils import is_jwt_format from pyeudiw.x509.verify import get_public_key_from_x509_chain +from pyeudiw.jwt.utils import decode_jwt_header, decode_jwt_payload KeyIdentifier_T = str @@ -24,10 +24,6 @@ def parse(jws: str) -> 'DecodedJwt': return unsafe_parse_jws(jws) -def _unsafe_decode_part(part: str) -> dict: - return json_decode(base64url_decode(part)) - - def unsafe_parse_jws(token: str) -> DecodedJwt: """Parse a token into it's component. Correctness of this function is not guaranteed when the token is in a @@ -35,12 +31,11 @@ def unsafe_parse_jws(token: str) -> DecodedJwt: """ if not is_jwt_format(token): raise ValueError(f"unable to parse {token}: not a jwt") - b64header, b64payload, signature, *_ = token.split(".") - head = {} - payload = {} + try: - head = _unsafe_decode_part(b64header) - payload = _unsafe_decode_part(b64payload) + head = decode_jwt_header(token) + payload = decode_jwt_payload(token) + signature = token.split(".")[2] except Exception as e: raise ValueError(f"unable to decode JWS part: {e}") return DecodedJwt(token, head, payload, signature=signature) From 7ea2acedfc6b73f33813ae975956afa72e708051 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Tue, 22 Oct 2024 18:33:29 +0200 Subject: [PATCH 03/44] fix: wrong key --- pyeudiw/jwt/parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeudiw/jwt/parse.py b/pyeudiw/jwt/parse.py index 5a4e8c11..e53b8402 100644 --- a/pyeudiw/jwt/parse.py +++ b/pyeudiw/jwt/parse.py @@ -46,7 +46,7 @@ def extract_key_identifier(token_header: dict) -> JWK | KeyIdentifier_T: if "kid" in token_header.keys(): return KeyIdentifier_T(token_header["kid"]) if "trust_chain" in token_header.keys(): - return get_public_key_from_trust_chain(token_header["kid"]) + return get_public_key_from_trust_chain(token_header["trust_chain"]) if "x5c" in token_header.keys(): return get_public_key_from_x509_chain(token_header["x5c"]) raise ValueError(f"unable to infer identifying key from token head: searched among keys {token_header.keys()}") From 97c9dd127a60f57cdd8848f9d9677af7cff33efe Mon Sep 17 00:00:00 2001 From: PascalDR Date: Tue, 22 Oct 2024 18:40:35 +0200 Subject: [PATCH 04/44] feat: decode_jwt_element refactoring --- pyeudiw/jwt/utils.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pyeudiw/jwt/utils.py b/pyeudiw/jwt/utils.py index a6c0c70e..0cd3e7d3 100644 --- a/pyeudiw/jwt/utils.py +++ b/pyeudiw/jwt/utils.py @@ -4,7 +4,7 @@ from typing import Dict from pyeudiw.jwk import find_jwk_by_kid -from pyeudiw.jwt.exceptions import JWTInvalidElementPosition +from pyeudiw.jwt.exceptions import JWTInvalidElementPosition, JWTDecodeError # jwt regexp pattern is non terminating, hence it match jwt, sd-jwt and sd-jwt with kb JWT_REGEXP = r'^[_\w\-]+\.[_\w\-]+\.[_\w\-]+' @@ -24,17 +24,25 @@ def decode_jwt_element(jwt: str, position: int) -> dict: :returns: a dict with the content of the decoded section. :rtype: dict """ - if position > 1 or position < 0: + if position < 0: + raise JWTInvalidElementPosition( + f"Cannot accept negative position {position}") + + splitted_jwt = jwt.split(".") + + if (len(splitted_jwt) - 1) < position: raise JWTInvalidElementPosition( f"JWT has no element in position {position}") - if isinstance(jwt, bytes): - jwt = jwt.decode() + try: + if isinstance(jwt, bytes): + jwt = jwt.decode() - b = jwt.split(".")[position] - padded = f"{b}{'=' * divmod(len(b), 4)[1]}" - data = json.loads(base64.urlsafe_b64decode(padded)) - return data + b64_data = jwt.split(".")[position] + data = json.loads(base64_urldecode(b64_data)) + return data + except Exception as e: + raise JWTDecodeError(f"Unable to decode JWT element: {e}") def decode_jwt_header(jwt: str) -> dict: From cbc1297156df23a3a7f9396757ccaca522e8f3ea Mon Sep 17 00:00:00 2001 From: PascalDR Date: Tue, 22 Oct 2024 18:41:26 +0200 Subject: [PATCH 05/44] chore: removed unused functions --- pyeudiw/jwt/utils.py | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/pyeudiw/jwt/utils.py b/pyeudiw/jwt/utils.py index 0cd3e7d3..acd343af 100644 --- a/pyeudiw/jwt/utils.py +++ b/pyeudiw/jwt/utils.py @@ -71,30 +71,6 @@ def decode_jwt_payload(jwt: str) -> dict: return decode_jwt_element(jwt, position=1) -def get_jwk_from_jwt(jwt: str, provider_jwks: Dict[str, dict]) -> dict: - """ - Find the JWK inside the provider JWKs with the kid - specified in jwt header. - - :param jwt: a string that represents the jwt. - :type jwt: str - :param provider_jwks: a dictionary that contains one or more JWKs with the KID as the key. - :type provider_jwks: Dict[str, dict] - - :raises InvalidKid: if kid is None. - :raises KidNotFoundError: if kid is not in jwks list. - - :returns: the jwk as dict. - :rtype: dict - """ - head = decode_jwt_header(jwt) - kid = head["kid"] - if isinstance(provider_jwks, dict) and provider_jwks.get('keys'): - provider_jwks = provider_jwks['keys'] - - return find_jwk_by_kid(kid, provider_jwks) - - def is_jwt_format(jwt: str) -> bool: """ Check if a string is in JWT format. @@ -132,22 +108,6 @@ def is_jwe_format(jwt: str): return True -def is_jws_format(jwt: str): - """ - Check if a string is in JWS format. - - :param jwt: a string that represents the jwt. - :type jwt: str - - :returns: True if the string is a JWS, False otherwise. - :rtype: bool - """ - if not is_jwt_format(jwt): - return False - - return not is_jwe_format(jwt) - - def base64_urlencode(v: bytes) -> str: """Urlsafe base64 encoding without padding symbols From a0a4c32618012a80974d22d689d97d9338009371 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Tue, 22 Oct 2024 18:47:35 +0200 Subject: [PATCH 06/44] fix: merged functions --- pyeudiw/jwt/verification.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pyeudiw/jwt/verification.py b/pyeudiw/jwt/verification.py index 89888aa2..8d6be6e6 100644 --- a/pyeudiw/jwt/verification.py +++ b/pyeudiw/jwt/verification.py @@ -4,7 +4,6 @@ from pyeudiw.jwt.utils import decode_jwt_payload from pyeudiw.tools.utils import iat_now - def verify_jws_with_key(jws: str, key: JWK) -> None: """ :raises JWSVerificationError: is signature verification fails for *any* reason @@ -15,16 +14,24 @@ def verify_jws_with_key(jws: str, key: JWK) -> None: except Exception as e: raise JWSVerificationError(f"error during signature verification: {e}", e) +def is_jwt_expired(token: str) -> bool: + """ + Check if a jwt is expired. + + :param token: a string that represents the jwt. + :type token: str + + :returns: True if the token is expired, False otherwise. + :rtype: bool + """ + + token_payload = decode_jwt_payload(token) -def is_payload_expired(token_payload: dict) -> bool: exp = token_payload.get("exp", None) if not exp: return True if exp < iat_now(): return True return False + - -def is_jwt_expired(token: str) -> bool: - payalod = decode_jwt_payload(token) - return is_payload_expired(payalod) From 8fc1fc3548c586112db4ad8a3c81781700f51d5d Mon Sep 17 00:00:00 2001 From: PascalDR Date: Tue, 22 Oct 2024 18:51:29 +0200 Subject: [PATCH 07/44] fix: find_vp_token_key refactoring --- pyeudiw/satosa/default/response_handler.py | 23 +++-------------- pyeudiw/tools/jwk_handling.py | 30 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 pyeudiw/tools/jwk_handling.py diff --git a/pyeudiw/satosa/default/response_handler.py b/pyeudiw/satosa/default/response_handler.py index 40c13667..7bc5ccaf 100644 --- a/pyeudiw/satosa/default/response_handler.py +++ b/pyeudiw/satosa/default/response_handler.py @@ -25,7 +25,7 @@ from pyeudiw.sd_jwt.schema import VerifierChallenge from pyeudiw.storage.exceptions import StorageWriteError from pyeudiw.tools.utils import iat_now -from pyeudiw.trust.interface import TrustEvaluator +from pyeudiw.tools.jwk_handling import find_vp_token_key class ResponseHandler(ResponseHandlerInterface, BackendTrust): @@ -184,7 +184,7 @@ def response_endpoint(self, context: Context, *args: tuple) -> Redirect | JsonRe token_parser, token_verifier = self._vp_verifier_factory(authz_payload.presentation_submission, vp_token, request_session) except ValueError as e: return self._handle_400(context, f"VP parsing error: {e}") - pub_jwk = _find_vp_token_key(token_parser, self.trust_evaluator) + pub_jwk = find_vp_token_key(token_parser, self.trust_evaluator) token_verifier.verify_signature(pub_jwk) try: token_verifier.verify_challenge() @@ -303,21 +303,4 @@ def _vp_verifier_factory(self, presentation_submission: dict, token: str, sessio return (token_processor, deepcopy(token_processor)) def _get_verifier_challenge(self, session_data: dict) -> VerifierChallenge: - return {"aud": self.client_id, "nonce": session_data["nonce"]} - - -def _find_vp_token_key(token_parser: VpTokenParser, key_source: TrustEvaluator) -> JWK: - # TODO: move somewhere appropriate: this doesn't HAVE to be in the response handler - issuer = token_parser.get_issuer_name() - trusted_pub_keys = key_source.get_public_keys(issuer) - verification_key = token_parser.get_signing_key() - if isinstance(verification_key, str): - # search by kid - kid = verification_key - pub_jwks = [key for key in trusted_pub_keys if key.get("kid", "") == kid] - if len(pub_jwks) != 1: - raise Exception(f"no unique valid trusted key with kid={kid} for issuer {issuer}") - return JWK(pub_jwks[0]) - if isinstance(verification_key, dict): - raise NotImplementedError("TODO: matching of public key (ex. from x5c) with keys from trust source") - raise Exception(f"invalid state: key with type {type(verification_key)}") + return {"aud": self.client_id, "nonce": session_data["nonce"]} \ No newline at end of file diff --git a/pyeudiw/tools/jwk_handling.py b/pyeudiw/tools/jwk_handling.py new file mode 100644 index 00000000..50e31ebf --- /dev/null +++ b/pyeudiw/tools/jwk_handling.py @@ -0,0 +1,30 @@ +from pyeudiw.jwk import JWK +from pyeudiw.openid4vp.interface import VpTokenParser +from pyeudiw.trust.interface import TrustEvaluator +from pyeudiw.jwk import find_jwk_by_kid + +def find_vp_token_key(token_parser: VpTokenParser, key_source: TrustEvaluator) -> JWK: + """ + :param token_parser: the token parser instance. + :type token_parser: VpTokenParser + :param key_source: the key source instance. + :type key_source: TrustEvaluator + + :raises KidNotFoundError: if no key is found. + :raises NotImplementedError: if the key is not in a comptible format. + + :returns: a JWK instance. + :rtype: JWK + """ + + issuer = token_parser.get_issuer_name() + trusted_pub_keys = key_source.get_public_keys(issuer) + verification_key = token_parser.get_signing_key() + + if isinstance(verification_key, str): + return find_jwk_by_kid(verification_key, trusted_pub_keys) + + if isinstance(verification_key, dict): + raise NotImplementedError("TODO: matching of public key (ex. from x5c) with keys from trust source") + + raise Exception(f"invalid state: key with type {type(verification_key)}") From 0333221fe9cfa7d2ed89b7c3dd97c512bdaf6465 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Tue, 22 Oct 2024 18:55:48 +0200 Subject: [PATCH 08/44] test: added tests for jwt package --- pyeudiw/tests/jwt/__init__.py | 3 ++ pyeudiw/tests/jwt/test_parse.py | 69 ++++++++++++++++++++++++++ pyeudiw/tests/jwt/test_utils.py | 62 +++++++++++++++++++++++ pyeudiw/tests/jwt/test_verification.py | 31 ++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 pyeudiw/tests/jwt/__init__.py create mode 100644 pyeudiw/tests/jwt/test_parse.py create mode 100644 pyeudiw/tests/jwt/test_utils.py create mode 100644 pyeudiw/tests/jwt/test_verification.py diff --git a/pyeudiw/tests/jwt/__init__.py b/pyeudiw/tests/jwt/__init__.py new file mode 100644 index 00000000..bffda586 --- /dev/null +++ b/pyeudiw/tests/jwt/__init__.py @@ -0,0 +1,3 @@ +VALID_KID_JWT = "eyJraWQiOiIxMjM0NTYiLCJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.bjM57L1H4gB60_020lKBVvVEhiYCOeEWGzMVEt-XNjc" +VALID_TC_JWT = "eyJ0cnVzdF9jaGFpbiI6WyJleUpoYkdjaU9pSkZVekkxTmlJc0ltdHBaQ0k2SW1Gck5WQk9NR1IxV2pOQ2VWbFZVa1ZOTVdzeldtMVJNMVJGVm5sT1NFb3dXVEpXVkZsVVdrMVRTRkkwVldzeFNWRXhRazlVU0VwUVUxRWlMQ0owZVhBaU9pSmxiblJwZEhrdGMzUmhkR1Z0Wlc1MEsycDNkQ0o5LmV5SmxlSEFpT2pFM01qazVNRFF6TkRJc0ltbGhkQ0k2TVRjeU9UWXdORE0wTWl3aWFYTnpJam9pYUhSMGNITTZMeTlqY21Wa1pXNTBhV0ZzWDJsemMzVmxjaTVsZUdGdGNHeGxMbTl5WnlJc0luTjFZaUk2SW1oMGRIQnpPaTh2WTNKbFpHVnVkR2xoYkY5cGMzTjFaWEl1WlhoaGJYQnNaUzV2Y21jaUxDSnFkMnR6SWpwN0ltdGxlWE1pT2x0N0ltdDBlU0k2SWtWRElpd2lhMmxrSWpvaVlXczFVRTR3WkhWYU0wSjVXVlZTUlUweGF6TmFiVkV6VkVWV2VVNUlTakJaTWxaVVdWUmFUVk5JVWpSVmF6RkpVVEZDVDFSSVNsQlRVU0lzSW1Gc1p5STZJa1ZUTWpVMklpd2lZM0oySWpvaVVDMHlOVFlpTENKNElqb2lZakJJY21WNmJUVnhOMU16VUU5NlpWTm9iVTlXUmpKVlYxOHpibkp2UjBSTlduQmFlRmhsUzFCMFVTSXNJbmtpT2lJdE1FOUhWMHhuT0dOb2FWSXRRbmRQUTJwWmVuZzFNbTFNWmxFMWIzQlNWalZZUTBsVmFtbHBhVlJSSW4xZGZTd2liV1YwWVdSaGRHRWlPbnNpYjNCbGJtbGtYMk55WldSbGJuUnBZV3hmYVhOemRXVnlJanA3SW1wM2EzTWlPbnNpYTJWNWN5STZXM3NpYTNSNUlqb2lSVU1pTENKcmFXUWlPaUpOYmxFMFZVZEtibVZXVWxkWU1EbDVaV3BDVUdJeWVEQlZNVTUwWVVaYWJGZ3dNVTlQVlRsSVUwZDBNVlZwTVU1TlJFNVZWMGRzVTFKUklpd2lZV3huSWpvaVJWTXlOVFlpTENKamNuWWlPaUpRTFRJMU5pSXNJbmdpT2lKNlZIQmpORFl4TjFkTFNVRjBVVVZYV2xsWWVERkZSalpHT0VwblYzb3pkSGxsYUhjNE1VSjNiRzg0SWl3aWVTSTZJbU5JVHkxRGFEWnNlVVV5WW13ek1UTnJlbFJoUzNKRWJDMTROM1pYYmtVMGRrVTBWVGRXVVVGNWFrMGlmVjE5ZlN3aVptVmtaWEpoZEdsdmJsOWxiblJwZEhraU9uc2liM0puWVc1cGVtRjBhVzl1WDI1aGJXVWlPaUpQY0dWdVNVUWdRM0psWkdWdWRHbGhiQ0JKYzNOMVpYSWdaWGhoYlhCc1pTSXNJbWh2YldWd1lXZGxYM1Z5YVNJNkltaDBkSEJ6T2k4dlkzSmxaR1Z1ZEdsaGJGOXBjM04xWlhJdVpYaGhiWEJzWlM1dmNtY3ZhRzl0WlNJc0luQnZiR2xqZVY5MWNta2lPaUpvZEhSd2N6b3ZMMk55WldSbGJuUnBZV3hmYVhOemRXVnlMbVY0WVcxd2JHVXViM0puTDNCdmJHbGplU0lzSW14dloyOWZkWEpwSWpvaWFIUjBjSE02THk5amNtVmtaVzUwYVdGc1gybHpjM1ZsY2k1bGVHRnRjR3hsTG05eVp5OXpkR0YwYVdNdmJHOW5ieTV6ZG1jaUxDSmpiMjUwWVdOMGN5STZXeUowWldOb1FHTnlaV1JsYm5ScFlXeGZhWE56ZFdWeUxtVjRZVzF3YkdVdWIzSm5JbDE5ZlN3aVlYVjBhRzl5YVhSNVgyaHBiblJ6SWpwYkltaDBkSEJ6T2k4dmFXNTBaWEp0WldScFlYUmxMbVZwWkdGekxtVjRZVzF3YkdVdWIzSm5JbDE5LmtlNThMQ1NTRnZ5aTZkYW9hUlIzNDZhRjNUQ240bENBODZHWEhoRmEwOXVWRTZHa3Q2alVKaEI4dEZsdnZkWmJlcmhxYnZhdG9HRUNQQ1BlQ0syNk13IiwiZXlKaGJHY2lPaUpGVXpJMU5pSXNJbXRwWkNJNklrOVVUblJUUlRneVZsZDRZV0ZxU2pGV2JHeFhXVlJTVUdKSVdrUlpibEY1VVd4d1ptUXlWbWxpUlUweVZFVndWR1ZxUms1V1dHUlNXbmNpTENKMGVYQWlPaUpsYm5ScGRIa3RjM1JoZEdWdFpXNTBLMnAzZENKOS5leUpsZUhBaU9qRTNNams1TURRek5ESXNJbWxoZENJNk1UY3lPVFl3TkRNME1pd2lhWE56SWpvaWFIUjBjSE02THk5cGJuUmxjbTFsWkdsaGRHVXVaV2xrWVhNdVpYaGhiWEJzWlM1dmNtY2lMQ0p6ZFdJaU9pSm9kSFJ3Y3pvdkwyTnlaV1JsYm5ScFlXeGZhWE56ZFdWeUxtVjRZVzF3YkdVdWIzSm5JaXdpYW5kcmN5STZleUpyWlhseklqcGJleUpyZEhraU9pSkZReUlzSW10cFpDSTZJbUZyTlZCT01HUjFXak5DZVZsVlVrVk5NV3N6V20xUk0xUkZWbmxPU0Vvd1dUSldWRmxVV2sxVFNGSTBWV3N4U1ZFeFFrOVVTRXBRVTFFaUxDSmhiR2NpT2lKRlV6STFOaUlzSW1OeWRpSTZJbEF0TWpVMklpd2llQ0k2SW1Jd1NISmxlbTAxY1RkVE0xQlBlbVZUYUcxUFZrWXlWVmRmTTI1eWIwZEVUVnB3V25oWVpVdFFkRkVpTENKNUlqb2lMVEJQUjFkTVp6aGphR2xTTFVKM1QwTnFXWHA0TlRKdFRHWlJOVzl3VWxZMVdFTkpWV3BwYVdsVVVTSjlYWDE5LjltMWk5cWNETFNucGJ3aU5iR1pKb3pvdlJUeGhGNlFiLUV2U1pmWU5lN2NzbmhZX2F1VERLRGllWW9aQmZhaW5ZR2lITTJ4dzk4LXdna3lnTFY3S0h3IiwiZXlKaGJHY2lPaUpGVXpJMU5pSXNJbXRwWkNJNklsWnRkekpaYkdjMFRWUnJOV05XYkhSaVJXeEhVa1JPVmsxNlp6UlVWMXBQVFRCR1VXTXdOREZqTTBKSlpGVmtjMWxZUm05VGJWSkxUa0VpTENKMGVYQWlPaUpsYm5ScGRIa3RjM1JoZEdWdFpXNTBLMnAzZENKOS5leUpsZUhBaU9qRTNNams1TURRek5ESXNJbWxoZENJNk1UY3lPVFl3TkRNME1pd2lhWE56SWpvaWFIUjBjSE02THk5MGNuVnpkQzFoYm1Ob2IzSXVaWGhoYlhCc1pTNXZjbWNpTENKemRXSWlPaUpvZEhSd2N6b3ZMMmx1ZEdWeWJXVmthV0YwWlM1bGFXUmhjeTVsZUdGdGNHeGxMbTl5WnlJc0ltcDNhM01pT25zaWEyVjVjeUk2VzNzaWEzUjVJam9pUlVNaUxDSnJhV1FpT2lKUFZFNTBVMFU0TWxaWGVHRmhha294Vm14c1YxbFVVbEJpU0ZwRVdXNVJlVkZzY0daa01sWnBZa1ZOTWxSRmNGUmxha1pPVmxoa1VscDNJaXdpWVd4bklqb2lSVk15TlRZaUxDSmpjbllpT2lKUUxUSTFOaUlzSW5naU9pSnJOMVJNV1ZGMVNYRTVlR05uYkdWU2QwNXZZWEJHYzFRMWVEVmpkM0IwT0V4U1QyZDFNRWhTWkU4d0lpd2llU0k2SWxoNE1UQmhXblp4ZUZGclZXeEdaVVF4ZGt4MWJuaFdTbmR2YkdacFVHeHFRaTF3T1hSZlkwaExPV01pZlYxOWZRLmI3eHlHdERwMi1aTVdsTkJOT2pFZVVnRUNMX29QN1RRamRIbGoybWVfWTZqc19BZW9FaGxRLTJlTXpXdGN1WUs0R1Y4eExHb0g3Q2xuN3BGSTFPeFRnIiwiZXlKaGJHY2lPaUpGVXpJMU5pSXNJbXRwWkNJNklsWnRkekpaYkdjMFRWUnJOV05XYkhSaVJXeEhVa1JPVmsxNlp6UlVWMXBQVFRCR1VXTXdOREZqTTBKSlpGVmtjMWxZUm05VGJWSkxUa0VpTENKMGVYQWlPaUpsYm5ScGRIa3RjM1JoZEdWdFpXNTBLMnAzZENKOS5leUpsZUhBaU9qRTNNams1TURRek5ESXNJbWxoZENJNk1UY3lPVFl3TkRNME1pd2lhWE56SWpvaWFIUjBjSE02THk5MGNuVnpkQzFoYm1Ob2IzSXVaWGhoYlhCc1pTNXZjbWNpTENKemRXSWlPaUpvZEhSd2N6b3ZMM1J5ZFhOMExXRnVZMmh2Y2k1bGVHRnRjR3hsTG05eVp5SXNJbXAzYTNNaU9uc2lhMlY1Y3lJNlczc2lhM1I1SWpvaVJVTWlMQ0pyYVdRaU9pSldiWGN5V1d4bk5FMVVhelZqVm14MFlrVnNSMUpFVGxaTmVtYzBWRmRhVDAwd1JsRmpNRFF4WXpOQ1NXUlZaSE5aV0VadlUyMVNTMDVCSWl3aVlXeG5Jam9pUlZNeU5UWWlMQ0pqY25ZaU9pSlFMVEkxTmlJc0luZ2lPaUpOUW14V1gxTm1YMU4yYVdzeFdqSjRaa3hrZGpKek5rZEhielp1UWxwWU1VTnBRVTlXV1Y5Q2EzTjNJaXdpZVNJNkltTkxkakV3WVRSblQySlZOVmx1YVUxMFpVMVFRVGRwWmpod2JEUnlaM2hUVFhKMGJDMVdOREJSVkhNaWZWMTlMQ0p0WlhSaFpHRjBZU0k2ZXlKbVpXUmxjbUYwYVc5dVgyVnVkR2wwZVNJNmV5Sm1aV1JsY21GMGFXOXVYMlpsZEdOb1gyVnVaSEJ2YVc1MElqb2lhSFIwY0hNNkx5OTBjblZ6ZEMxaGJtTm9iM0l1WlhoaGJYQnNaUzV2Y21jdlptVjBZMmdpTENKbVpXUmxjbUYwYVc5dVgzSmxjMjlzZG1WZlpXNWtjRzlwYm5RaU9pSm9kSFJ3Y3pvdkwzUnlkWE4wTFdGdVkyaHZjaTVsZUdGdGNHeGxMbTl5Wnk5eVpYTnZiSFpsSWl3aVptVmtaWEpoZEdsdmJsOXNhWE4wWDJWdVpIQnZhVzUwSWpvaWFIUjBjSE02THk5MGNuVnpkQzFoYm1Ob2IzSXVaWGhoYlhCc1pTNXZjbWN2YkdsemRDSXNJbTl5WjJGdWFYcGhkR2x2Ymw5dVlXMWxJam9pVkVFZ1pYaGhiWEJzWlNJc0ltaHZiV1Z3WVdkbFgzVnlhU0k2SW1oMGRIQnpPaTh2ZEhKMWMzUXRZVzVqYUc5eUxtVjRZVzF3YkdVdWIzSm5MMmh2YldVaUxDSndiMnhwWTNsZmRYSnBJam9pYUhSMGNITTZMeTkwY25WemRDMWhibU5vYjNJdVpYaGhiWEJzWlM1dmNtY3ZjRzlzYVdONUlpd2liRzluYjE5MWNta2lPaUpvZEhSd2N6b3ZMM1J5ZFhOMExXRnVZMmh2Y2k1bGVHRnRjR3hsTG05eVp5OXpkR0YwYVdNdmJHOW5ieTV6ZG1jaUxDSmpiMjUwWVdOMGN5STZXeUowWldOb1FIUnlkWE4wTFdGdVkyaHZjaTVsZUdGdGNHeGxMbTl5WnlKZGZYMHNJbU52Ym5OMGNtRnBiblJ6SWpwN0ltMWhlRjl3WVhSb1gyeGxibWQwYUNJNk1YMTkuTWJwWGZlX05wUGdiZFdMX3pOMzBTWEE4OGFXcmV3YUp5TVdKRkFlZ05yTi04VnkydW1jcHEzTVFwaDdZejNaVGF3R2dpNk9HV1g3VVRERk9XV21mOXciXSwiYWxnIjoiSFMyNTYiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.9SGvKJ6ucPo8pkxpg-VXIYwijHH6jyOeqezgkFP74ow" +VALID_JWE = "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiIxOGIxY2Y3NThjMWQ0ZWM2YmRhNjU4OTM1N2FiZGQ4NSIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9.gCbxP78o3DgpDTUQbuHniuGgYpATqgGkRGy7paC6hRrz7N7eIa6sAOWDO9Fhnj-c8ocMl4cF4Jb_mv5qRPCh9r57PBqx7jOhMIMPTwJGpjcyBaqtHlZlu1vupY5tQ3Y2jGz1Ti4BnywaeEHPyIPQJtN7F7hIAORzj7IY4sIKkVXtQJZgaKW8pEHq_GCqj8i5aaiM0uJnRG3GOh3livp9Npjv9doqp3gyPa1zjrg2H1RsOGn0j2QMGvtuVfkuNwF-SoPKFECyHOq0ZK1oH2sTO8-JwvHflbIZQr5xWTpS8q7MbUXEuqURtrg0Tj-2z6tdaOLT4b3UeDufK2ar3bBfRD4-nRALtoY0ekcMyGFOS7o1Mxl3hy5sIG-EySyWeuBVy68aDWDpi9qZoQuY1TbxxakjncCOGu_Gh1l1m_mK2l_IdyXCT_GCfzFq4ZTkPZ5eydNBAPZuxBLUb4BrMb5iDdZjT7AgGOlRre_wIRHmmKm8W9nDeQQRmbIXO23JuOw9.BDCarfq2r_Uk8DHNfsNwSQ.4DuQx1cfJXadHnudrVaBss45zxyd6iouuSzZUyOeM4ikF_7hDOgwmaCma-Z97_QZBJ5DzVn9SJhKUTAqpVR3BRGAxJ_HAXU5jaTjXqbvUaxsh7Z5TgZ9eck0FIoe1lkwv51xEvYqqQ_Xojr4MAEmLuME_9ArCK9mNaMADIzOj4VoQtaDP1l26ytocc-oENifBRYGu28LbJLkyQKzyQy6FuAOtWjLM0WCXV7-o_dvj6qfeYHNBD7YBSxyqdgD8dcxMBNd2sK73YsZPHEa0V1-8zz7hm3bH3tZelpwPWScqLLW_SUH586c0FVeI6ggvqzjfLZ_Y6eQibVSdXfOtJBk22QrLsuCXbRK8G1w9t23Pwu8ukUAw4v0l7HeaW_0SJyKSPQANRP83MyFbK7fmzTYaW9TYN2JrKN-PLpd2dIFSm2Ga_EfaCwNJBm4RDMzDNrf-O0AissvYyHb0WaALiCiFCogliYqLzRB6xDb-b4964M.J7WDOFLRRPJ7lLpTfN2mOiXLDg5xtaF-sLQ4mOeN5oc" \ No newline at end of file diff --git a/pyeudiw/tests/jwt/test_parse.py b/pyeudiw/tests/jwt/test_parse.py new file mode 100644 index 00000000..fffb6d07 --- /dev/null +++ b/pyeudiw/tests/jwt/test_parse.py @@ -0,0 +1,69 @@ +from pyeudiw.jwt.parse import DecodedJwt, extract_key_identifier +from pyeudiw.tests.jwt import VALID_KID_JWT, VALID_TC_JWT + +def test_kid_jwt(): + decoded_jwt = DecodedJwt.parse(VALID_KID_JWT) + + assert decoded_jwt.jwt == VALID_KID_JWT + assert decoded_jwt.header == { + "kid": "123456", + "alg": "HS256", + "typ": "JWT" + } + assert decoded_jwt.payload == { + "sub": "1234567890", + "name": "John Doe", + "iat": 1516239022 + } + assert decoded_jwt.signature == "bjM57L1H4gB60_020lKBVvVEhiYCOeEWGzMVEt-XNjc" + +def test_tc_jwt(): + decoded_jwt = DecodedJwt.parse(VALID_TC_JWT) + + assert decoded_jwt.jwt == VALID_TC_JWT + assert decoded_jwt.header == { + "trust_chain": [ + "eyJhbGciOiJFUzI1NiIsImtpZCI6ImFrNVBOMGR1WjNCeVlVUkVNMWszWm1RM1RFVnlOSEowWTJWVFlUWk1TSFI0VWsxSVExQk9USEpQU1EiLCJ0eXAiOiJlbnRpdHktc3RhdGVtZW50K2p3dCJ9.eyJleHAiOjE3Mjk5MDQzNDIsImlhdCI6MTcyOTYwNDM0MiwiaXNzIjoiaHR0cHM6Ly9jcmVkZW50aWFsX2lzc3Vlci5leGFtcGxlLm9yZyIsInN1YiI6Imh0dHBzOi8vY3JlZGVudGlhbF9pc3N1ZXIuZXhhbXBsZS5vcmciLCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IkVDIiwia2lkIjoiYWs1UE4wZHVaM0J5WVVSRU0xazNabVEzVEVWeU5ISjBZMlZUWVRaTVNIUjRVazFJUTFCT1RISlBTUSIsImFsZyI6IkVTMjU2IiwiY3J2IjoiUC0yNTYiLCJ4IjoiYjBIcmV6bTVxN1MzUE96ZVNobU9WRjJVV18zbnJvR0RNWnBaeFhlS1B0USIsInkiOiItME9HV0xnOGNoaVItQndPQ2pZeng1Mm1MZlE1b3BSVjVYQ0lVamlpaVRRIn1dfSwibWV0YWRhdGEiOnsib3BlbmlkX2NyZWRlbnRpYWxfaXNzdWVyIjp7Imp3a3MiOnsia2V5cyI6W3sia3R5IjoiRUMiLCJraWQiOiJNblE0VUdKbmVWUldYMDl5ZWpCUGIyeDBVMU50YUZabFgwMU9PVTlIU0d0MVVpMU5NRE5VV0dsU1JRIiwiYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsIngiOiJ6VHBjNDYxN1dLSUF0UUVXWllYeDFFRjZGOEpnV3ozdHllaHc4MUJ3bG84IiwieSI6ImNITy1DaDZseUUyYmwzMTNrelRhS3JEbC14N3ZXbkU0dkU0VTdWUUF5ak0ifV19fSwiZmVkZXJhdGlvbl9lbnRpdHkiOnsib3JnYW5pemF0aW9uX25hbWUiOiJPcGVuSUQgQ3JlZGVudGlhbCBJc3N1ZXIgZXhhbXBsZSIsImhvbWVwYWdlX3VyaSI6Imh0dHBzOi8vY3JlZGVudGlhbF9pc3N1ZXIuZXhhbXBsZS5vcmcvaG9tZSIsInBvbGljeV91cmkiOiJodHRwczovL2NyZWRlbnRpYWxfaXNzdWVyLmV4YW1wbGUub3JnL3BvbGljeSIsImxvZ29fdXJpIjoiaHR0cHM6Ly9jcmVkZW50aWFsX2lzc3Vlci5leGFtcGxlLm9yZy9zdGF0aWMvbG9nby5zdmciLCJjb250YWN0cyI6WyJ0ZWNoQGNyZWRlbnRpYWxfaXNzdWVyLmV4YW1wbGUub3JnIl19fSwiYXV0aG9yaXR5X2hpbnRzIjpbImh0dHBzOi8vaW50ZXJtZWRpYXRlLmVpZGFzLmV4YW1wbGUub3JnIl19.ke58LCSSFvyi6daoaRR346aF3TCn4lCA86GXHhFa09uVE6Gkt6jUJhB8tFlvvdZberhqbvatoGECPCPeCK26Mw", + "eyJhbGciOiJFUzI1NiIsImtpZCI6Ik9UTnRTRTgyVld4YWFqSjFWbGxXWVRSUGJIWkRZblF5UWxwZmQyVmliRU0yVEVwVGVqRk5WWGRSWnciLCJ0eXAiOiJlbnRpdHktc3RhdGVtZW50K2p3dCJ9.eyJleHAiOjE3Mjk5MDQzNDIsImlhdCI6MTcyOTYwNDM0MiwiaXNzIjoiaHR0cHM6Ly9pbnRlcm1lZGlhdGUuZWlkYXMuZXhhbXBsZS5vcmciLCJzdWIiOiJodHRwczovL2NyZWRlbnRpYWxfaXNzdWVyLmV4YW1wbGUub3JnIiwiandrcyI6eyJrZXlzIjpbeyJrdHkiOiJFQyIsImtpZCI6ImFrNVBOMGR1WjNCeVlVUkVNMWszWm1RM1RFVnlOSEowWTJWVFlUWk1TSFI0VWsxSVExQk9USEpQU1EiLCJhbGciOiJFUzI1NiIsImNydiI6IlAtMjU2IiwieCI6ImIwSHJlem01cTdTM1BPemVTaG1PVkYyVVdfM25yb0dETVpwWnhYZUtQdFEiLCJ5IjoiLTBPR1dMZzhjaGlSLUJ3T0NqWXp4NTJtTGZRNW9wUlY1WENJVWppaWlUUSJ9XX19.9m1i9qcDLSnpbwiNbGZJozovRTxhF6Qb-EvSZfYNe7csnhY_auTDKDieYoZBfainYGiHM2xw98-wgkygLV7KHw", + "eyJhbGciOiJFUzI1NiIsImtpZCI6IlZtdzJZbGc0TVRrNWNWbHRiRWxHUkROVk16ZzRUV1pPTTBGUWMwNDFjM0JJZFVkc1lYRm9TbVJLTkEiLCJ0eXAiOiJlbnRpdHktc3RhdGVtZW50K2p3dCJ9.eyJleHAiOjE3Mjk5MDQzNDIsImlhdCI6MTcyOTYwNDM0MiwiaXNzIjoiaHR0cHM6Ly90cnVzdC1hbmNob3IuZXhhbXBsZS5vcmciLCJzdWIiOiJodHRwczovL2ludGVybWVkaWF0ZS5laWRhcy5leGFtcGxlLm9yZyIsImp3a3MiOnsia2V5cyI6W3sia3R5IjoiRUMiLCJraWQiOiJPVE50U0U4MlZXeGFhakoxVmxsV1lUUlBiSFpEWW5ReVFscGZkMlZpYkVNMlRFcFRlakZOVlhkUlp3IiwiYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsIngiOiJrN1RMWVF1SXE5eGNnbGVSd05vYXBGc1Q1eDVjd3B0OExST2d1MEhSZE8wIiwieSI6Ilh4MTBhWnZxeFFrVWxGZUQxdkx1bnhWSndvbGZpUGxqQi1wOXRfY0hLOWMifV19fQ.b7xyGtDp2-ZMWlNBNOjEeUgECL_oP7TQjdHlj2me_Y6js_AeoEhlQ-2eMzWtcuYK4GV8xLGoH7Cln7pFI1OxTg", + "eyJhbGciOiJFUzI1NiIsImtpZCI6IlZtdzJZbGc0TVRrNWNWbHRiRWxHUkROVk16ZzRUV1pPTTBGUWMwNDFjM0JJZFVkc1lYRm9TbVJLTkEiLCJ0eXAiOiJlbnRpdHktc3RhdGVtZW50K2p3dCJ9.eyJleHAiOjE3Mjk5MDQzNDIsImlhdCI6MTcyOTYwNDM0MiwiaXNzIjoiaHR0cHM6Ly90cnVzdC1hbmNob3IuZXhhbXBsZS5vcmciLCJzdWIiOiJodHRwczovL3RydXN0LWFuY2hvci5leGFtcGxlLm9yZyIsImp3a3MiOnsia2V5cyI6W3sia3R5IjoiRUMiLCJraWQiOiJWbXcyWWxnNE1UazVjVmx0YkVsR1JETlZNemc0VFdaT00wRlFjMDQxYzNCSWRVZHNZWEZvU21SS05BIiwiYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsIngiOiJNQmxWX1NmX1N2aWsxWjJ4ZkxkdjJzNkdHbzZuQlpYMUNpQU9WWV9Ca3N3IiwieSI6ImNLdjEwYTRnT2JVNVluaU10ZU1QQTdpZjhwbDRyZ3hTTXJ0bC1WNDBRVHMifV19LCJtZXRhZGF0YSI6eyJmZWRlcmF0aW9uX2VudGl0eSI6eyJmZWRlcmF0aW9uX2ZldGNoX2VuZHBvaW50IjoiaHR0cHM6Ly90cnVzdC1hbmNob3IuZXhhbXBsZS5vcmcvZmV0Y2giLCJmZWRlcmF0aW9uX3Jlc29sdmVfZW5kcG9pbnQiOiJodHRwczovL3RydXN0LWFuY2hvci5leGFtcGxlLm9yZy9yZXNvbHZlIiwiZmVkZXJhdGlvbl9saXN0X2VuZHBvaW50IjoiaHR0cHM6Ly90cnVzdC1hbmNob3IuZXhhbXBsZS5vcmcvbGlzdCIsIm9yZ2FuaXphdGlvbl9uYW1lIjoiVEEgZXhhbXBsZSIsImhvbWVwYWdlX3VyaSI6Imh0dHBzOi8vdHJ1c3QtYW5jaG9yLmV4YW1wbGUub3JnL2hvbWUiLCJwb2xpY3lfdXJpIjoiaHR0cHM6Ly90cnVzdC1hbmNob3IuZXhhbXBsZS5vcmcvcG9saWN5IiwibG9nb191cmkiOiJodHRwczovL3RydXN0LWFuY2hvci5leGFtcGxlLm9yZy9zdGF0aWMvbG9nby5zdmciLCJjb250YWN0cyI6WyJ0ZWNoQHRydXN0LWFuY2hvci5leGFtcGxlLm9yZyJdfX0sImNvbnN0cmFpbnRzIjp7Im1heF9wYXRoX2xlbmd0aCI6MX19.MbpXfe_NpPgbdWL_zN30SXA88aWrewaJyMWJFAegNrN-8Vy2umcpq3MQph7Yz3ZTawGgi6OGWX7UTDFOWWmf9w" + ], + "alg": "HS256", + "typ": "JWT" + } + +def test_invalid_jwt(): + invalid_jwt = "eyJ" + + try: + DecodedJwt.parse(invalid_jwt) + assert False + except ValueError: + assert True + +def test_extract_key_identifier(): + token_header = { + "kid": "123456" + } + + assert extract_key_identifier(token_header) == "123456" + +def test_extract_key_identifier_invalid(): + token_header = { + "invalid": "123456" + } + + try: + extract_key_identifier(token_header) + assert False + except ValueError: + assert True + + +def test_extract_key_identifier_tc(): + #TODO: Implement more accurate tests after implementing get_public_key_from_trust_chain and get_public_key_from_x509_chain + pass + +def test_extract_key_identifier_x5c(): + #TODO: Implement more accurate tests after implementing get_public_key_from_trust_chain and get_public_key_from_x509_chain + pass diff --git a/pyeudiw/tests/jwt/test_utils.py b/pyeudiw/tests/jwt/test_utils.py new file mode 100644 index 00000000..c13f2b3c --- /dev/null +++ b/pyeudiw/tests/jwt/test_utils.py @@ -0,0 +1,62 @@ +from pyeudiw.tests.jwt import VALID_TC_JWT, VALID_JWE +from pyeudiw.jwt.exceptions import JWTInvalidElementPosition, JWTDecodeError + +from pyeudiw.jwt.utils import decode_jwt_element, decode_jwt_header, decode_jwt_payload, is_jwt_format, is_jwe_format + +def test_decode_jwt_element(): + payload = decode_jwt_element(VALID_TC_JWT, 1) + assert payload + assert payload["sub"] == "1234567890" + assert payload["name"] == "John Doe" + assert payload["iat"] == 1516239022 + + header = decode_jwt_element(VALID_TC_JWT, 0) + assert header + assert header["alg"] == "HS256" + assert header["typ"] == "JWT" + +def test_decode_jwt_element_signature_failure(): + try: + decode_jwt_element(VALID_TC_JWT, 2) + assert False + except JWTDecodeError: + assert True + +def test_decode_jwt_element_invalid(): + try: + decode_jwt_element(VALID_TC_JWT, -1) + assert False + except JWTInvalidElementPosition: + assert True + + try: + decode_jwt_element(VALID_TC_JWT, 3) + assert False + except JWTInvalidElementPosition: + assert True + +def test_decode_jwt_header(): + header = decode_jwt_header(VALID_TC_JWT) + assert header + assert header["alg"] == "HS256" + assert header["typ"] == "JWT" + +def test_decode_jwt_payload(): + payload = decode_jwt_payload(VALID_TC_JWT) + assert payload + assert payload["sub"] == "1234567890" + assert payload["name"] == "John Doe" + assert payload["iat"] == 1516239022 + +def test_is_jwt_format(): + assert is_jwt_format(VALID_TC_JWT) + +def test_is_jwt_format_invalid(): + assert not is_jwt_format("eyJ") + +def test_is_jwe_format(): + assert is_jwe_format(VALID_JWE) + +def test_is_not_jwt_format_jwe(): + assert not is_jwe_format(VALID_TC_JWT) + diff --git a/pyeudiw/tests/jwt/test_verification.py b/pyeudiw/tests/jwt/test_verification.py new file mode 100644 index 00000000..39d02d34 --- /dev/null +++ b/pyeudiw/tests/jwt/test_verification.py @@ -0,0 +1,31 @@ +from pyeudiw.jwt.verification import is_jwt_expired, verify_jws_with_key +from pyeudiw.jwk import JWK +from pyeudiw.jwt import JWSHelper + +def test_is_jwt_expired(): + jwk = JWK(key_type="EC") + payload = {"exp": 1516239022} + + helper = JWSHelper(jwk) + jws = helper.sign(payload) + + assert is_jwt_expired(jws) == True + +def test_is_jwt_not_expired(): + jwk = JWK(key_type="EC") + payload = {"exp": 999999999999} + + helper = JWSHelper(jwk) + jws = helper.sign(payload) + + assert is_jwt_expired(jws) == False + +def test_verify_jws_with_key(): + jwk = JWK(key_type="EC") + payload = {"exp": 1516239022} + + helper = JWSHelper(jwk) + jws = helper.sign(payload) + + assert verify_jws_with_key(jws, jwk) == None + From f4a61e2721d3323d097ac92d5f13172d897c3da6 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Tue, 22 Oct 2024 18:58:41 +0200 Subject: [PATCH 09/44] fix: added boundary check --- pyeudiw/jwt/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyeudiw/jwt/utils.py b/pyeudiw/jwt/utils.py index acd343af..fb867a20 100644 --- a/pyeudiw/jwt/utils.py +++ b/pyeudiw/jwt/utils.py @@ -27,6 +27,10 @@ def decode_jwt_element(jwt: str, position: int) -> dict: if position < 0: raise JWTInvalidElementPosition( f"Cannot accept negative position {position}") + + if position > 2: + raise JWTInvalidElementPosition( + f"Cannot accept position greater than 2 {position}") splitted_jwt = jwt.split(".") From 655b13e4f186389e0b0eb7bab7a5a096aa604f21 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Wed, 23 Oct 2024 18:22:18 +0200 Subject: [PATCH 10/44] feat: added as_public_dict --- pyeudiw/jwk/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyeudiw/jwk/__init__.py b/pyeudiw/jwk/__init__.py index 91ed1fe0..a27dc5a7 100644 --- a/pyeudiw/jwk/__init__.py +++ b/pyeudiw/jwk/__init__.py @@ -116,6 +116,15 @@ def as_dict(self) -> dict: :rtype: dict """ return self.jwk + + def as_public_dict(self) -> dict: + """ + Returns the public key in format of dict. + + :returns: The public key in form of dict. + :rtype: dict + """ + return self.public_key def __repr__(self): # private part! From 9bc04936f730d70b57f872d8f03c75ad6e0709b0 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Wed, 23 Oct 2024 18:23:22 +0200 Subject: [PATCH 11/44] feat: refactoring --- pyeudiw/trust/default/federation.py | 46 ++++------------------------- pyeudiw/trust/dynamic.py | 42 +++++++------------------- pyeudiw/trust/interface.py | 9 ++++++ 3 files changed, 26 insertions(+), 71 deletions(-) diff --git a/pyeudiw/trust/default/federation.py b/pyeudiw/trust/default/federation.py index b277b5e9..2803b24c 100644 --- a/pyeudiw/trust/default/federation.py +++ b/pyeudiw/trust/default/federation.py @@ -13,8 +13,6 @@ from pyeudiw.satosa.exceptions import (DiscoveryFailedError, NotTrustedFederationError) from pyeudiw.storage.exceptions import EntryNotFound -from pyeudiw.tools.base_logger import BaseLogger -from pyeudiw.tools.utils import exp_from_now, iat_now from pyeudiw.trust import TrustEvaluationHelper from pyeudiw.trust.trust_anchors import update_trust_anchors_ecs @@ -32,8 +30,14 @@ class FederationTrustModel(TrustEvaluator): def __init__(self, **kwargs): # TODO; qui c'è dentro tutta la ciccia: trust chain verification, root of trust, etc self.metadata_policy_resolver = TrustChainPolicy() + self.federation_jwks = kwargs.get("federation_jwks", []) pass + def get_public_keys(self, issuer): + public_keys = [JWK(i).as_public_dict() for i in self.federation_jwks] + + return public_keys + def _verify_trust_chain(self, trust_chain: list[str]): # TODO: qui c'è tutta la ciccia, ma si può fare copia incolla da terze parti (specialmente di pyeudiw.trust.__init__) raise NotImplementedError @@ -245,41 +249,3 @@ def _validate_trust(self, context: Context, jws: str) -> TrustEvaluationHelper: ) return trust_eval - - # @property - # def default_federation_private_jwk(self) -> dict: - # """Returns the default federation private jwk.""" - # return tuple(self.federations_jwks_by_kids.values())[0] - - # @property - # def entity_configuration_as_dict(self) -> dict: - # """Returns the entity configuration as a dictionary.""" - # ec_payload = { - # "exp": exp_from_now(minutes=self.default_exp), - # "iat": iat_now(), - # "iss": self.client_id, - # "sub": self.client_id, - # "jwks": { - # "keys": self.federation_public_jwks - # }, - # "metadata": { - # self.config['trust']['federation']['config']["metadata_type"]: self.config['metadata'], - # "federation_entity": self.config['trust']['federation']['config']['federation_entity_metadata'] - # }, - # "authority_hints": self.config['trust']['federation']['config']['authority_hints'] - # } - # return ec_payload - - # @property - # def entity_configuration(self) -> dict: - # """Returns the entity configuration as a JWT.""" - # data = self.entity_configuration_as_dict - # jwshelper = JWSHelper(self.default_federation_private_jwk) - # return jwshelper.sign( - # protected={ - # "alg": self.config['trust']['federation']['config']["default_sig_alg"], - # "kid": self.default_federation_private_jwk["kid"], - # "typ": "entity-statement+jwt" - # }, - # plain_dict=data - # ) diff --git a/pyeudiw/trust/dynamic.py b/pyeudiw/trust/dynamic.py index 57f0f9de..ff8dbb9a 100644 --- a/pyeudiw/trust/dynamic.py +++ b/pyeudiw/trust/dynamic.py @@ -14,6 +14,7 @@ from pyeudiw.trust.exceptions import TrustConfigurationError from pyeudiw.trust.interface import TrustEvaluator from pyeudiw.trust._log import _package_logger +from pyeudiw.tools.utils import dynamic_class_loader TrustModuleConfiguration_T = TypedDict("_DynamicTrustConfiguration", {"module": str, "class": str, "config": dict}) @@ -37,50 +38,29 @@ def dynamic_trust_evaluators_loader(trust_config: dict[str, TrustModuleConfigura for trust_model_name, trust_module_config in trust_config.items(): try: - uninstantiated_class: type[TrustEvaluator] = get_dynamic_class(trust_module_config["module"], trust_module_config["class"]) - class_config: dict = trust_module_config["config"] - trust_evaluator_instance = uninstantiated_class(**class_config) + trust_evaluator_instance = dynamic_class_loader(trust_module_config["module"], trust_module_config["class"], trust_module_config["config"]) except Exception as e: raise TrustConfigurationError(f"invalid configuration for {trust_model_name}: {e}", e) + if not satisfy_interface(trust_evaluator_instance, TrustEvaluator): - raise TrustConfigurationError(f"class {uninstantiated_class} does not satisfy the interface TrustEvaluator") + raise TrustConfigurationError(f"class {trust_evaluator_instance.__class__} does not satisfy the interface TrustEvaluator") + trust_instances[trust_model_name] = trust_evaluator_instance return trust_instances class CombinedTrustEvaluator(TrustEvaluator, BaseLogger): - """CombinedTrustEvaluator is a wrapper around multiple implementations of + """ + CombinedTrustEvaluator is a wrapper around multiple implementations of TrustEvaluator. It's primary purpose is to handle how multiple configured trust sources are queried when some metadata or key material is requested. """ - def __init__(self, trust_evaluators: dict[str, TrustEvaluator], storage: Optional[DBEngine] = None): + def __init__(self, trust_evaluators: dict[str, TrustEvaluator], client_id: str): self.trust_evaluators: dict[str, TrustEvaluator] = trust_evaluators - self.storage: DBEngine | None = storage def _get_trust_identifier_names(self) -> str: return f'[{",".join(self.trust_evaluators.keys())}]' - - def _get_public_keys_from_storage(self, eval_identifier: str, issuer: str) -> dict | None: - # note: keys are serialized as jwks - if trust_attestation := self.storage.get_trust_attestation(issuer): - if trust_entity := trust_attestation.get(eval_identifier, None): - if trust_entity_jwks := trust_entity.get("jwks", None): - new_pks = trust_entity_jwks - # TODO: check if cached key is still valid? - # with mongodb we use ttl integrated in the engine - return new_pks - return None - - def _get_public_keys(self, eval_identifier: str, eval_instance: TrustEvaluator, issuer: str) -> dict: - try: - new_pks = eval_instance.get_public_keys(issuer) - self.storage.add_or_update_trust_attestation(issuer, trust_type=TrustType(eval_identifier), jwks=new_pks) - except: - new_pks = self._get_public_keys_from_storage(eval_identifier, issuer) - - if new_pks: return new_pks - else: raise Exception def get_public_keys(self, issuer: str) -> list[dict]: """ @@ -92,7 +72,7 @@ def get_public_keys(self, issuer: str) -> list[dict]: pks: list[dict] = [] for eval_identifier, eval_instance in self.trust_evaluators.items(): try: - new_pks = self._get_public_keys(eval_identifier, eval_instance, issuer) + new_pks = eval_instance.get_public_keys(issuer) except Exception as e: self._log_warning(f"failed to find any key of issuer {issuer} with model {eval_identifier}: {eval_instance.__class__.__name__}", e) continue @@ -108,8 +88,8 @@ def get_metadata(self, issuer: str) -> dict: trust model. """ md: dict = {} - for eval_identifier, eval_instance in self.trust_evaluators.items(): - md = eval_instance.get_metadata(issuer) + for instance in self.trust_evaluators.values(): + md = instance.get_metadata(issuer) if md: return md if not md: diff --git a/pyeudiw/trust/interface.py b/pyeudiw/trust/interface.py index e38f8756..9b47a31a 100644 --- a/pyeudiw/trust/interface.py +++ b/pyeudiw/trust/interface.py @@ -7,6 +7,12 @@ class that, as the very core, can: (2) obtain the meta information about an issuer that is defined according to some trust model """ + def initialize_istance(self, issuer: str) -> None: + """ + Initialize the cryptographic material of the issuer, according to some + trust model. + """ + raise NotImplementedError def get_public_keys(self, issuer: str) -> list[dict]: """ @@ -33,3 +39,6 @@ def is_revoked(self, issuer: str) -> bool: def get_policies(self, issuer: str) -> dict: raise NotImplementedError("reserved for future uses") + + def get_protected_header_resource(self): + raise NotImplementedError From 184fb36dcd2bde180a27075e856038525e2fa5e5 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Wed, 23 Oct 2024 18:24:03 +0200 Subject: [PATCH 12/44] tests: added federation to the config --- pyeudiw/tests/trust/test_dynamic.py | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pyeudiw/tests/trust/test_dynamic.py b/pyeudiw/tests/trust/test_dynamic.py index 3778d555..19bd7449 100644 --- a/pyeudiw/tests/trust/test_dynamic.py +++ b/pyeudiw/tests/trust/test_dynamic.py @@ -57,8 +57,45 @@ def test_trust_evaluators_loader(): } } } + }, + "federation": { + "module": "pyeudiw.trust.default.federation", + "class": "FederationTrustModel", + "config": { + "metadata_type": "wallet_relying_party", + "authority_hints": [ + "http://127.0.0.1:8000" + ], + "trust_anchors": [ + { + "public_keys": [] + }, + "http://127.0.0.1:8000" + ], + "default_sig_alg": "RS256", + "trust_marks": [], + "federation_entity_metadata": { + "organization_name": "Developers Italia SATOSA OpenID4VP backend", + "homepage_uri": "https://developers.italia.it", + "policy_uri": "https://developers.italia.it", + "tos_uri": "https://developers.italia.it", + "logo_uri": "https://developers.italia.it/assets/icons/logo-it.svg" + }, + "federation_jwks": [ + { + "kty": "RSA", + "d": "QUZsh1NqvpueootsdSjFQz-BUvxwd3Qnzm5qNb-WeOsvt3rWMEv0Q8CZrla2tndHTJhwioo1U4NuQey7znijhZ177bUwPPxSW1r68dEnL2U74nKwwoYeeMdEXnUfZSPxzs7nY6b7vtyCoA-AjiVYFOlgKNAItspv1HxeyGCLhLYhKvS_YoTdAeLuegETU5D6K1xGQIuw0nS13Icjz79Y8jC10TX4FdZwdX-NmuIEDP5-s95V9DMENtVqJAVE3L-wO-NdDilyjyOmAbntgsCzYVGH9U3W_djh4t3qVFCv3r0S-DA2FD3THvlrFi655L0QHR3gu_Fbj3b9Ybtajpue_Q", + "e": "AQAB", + "kid": "9Cquk0X-fNPSdePQIgQcQZtD6J0IjIRrFigW2PPK_-w", + "n": "utqtxbs-jnK0cPsV7aRkkZKA9t4S-WSZa3nCZtYIKDpgLnR_qcpeF0diJZvKOqXmj2cXaKFUE-8uHKAHo7BL7T-Rj2x3vGESh7SG1pE0thDGlXj4yNsg0qNvCXtk703L2H3i1UXwx6nq1uFxD2EcOE4a6qDYBI16Zl71TUZktJwmOejoHl16CPWqDLGo9GUSk_MmHOV20m4wXWkB4qbvpWVY8H6b2a0rB1B1YPOs5ZLYarSYZgjDEg6DMtZ4NgiwZ-4N1aaLwyO-GLwt9Vf-NBKwoxeRyD3zWE2FXRFBbhKGksMrCGnFDsNl5JTlPjaM3kYyImE941ggcuc495m-Fw", + "p": "2zmGXIMCEHPphw778YjVTar1eycih6fFSJ4I4bl1iq167GqO0PjlOx6CZ1-OdBTVU7HfrYRiUK_BnGRdPDn-DQghwwkB79ZdHWL14wXnpB5y-boHz_LxvjsEqXtuQYcIkidOGaMG68XNT1nM4F9a8UKFr5hHYT5_UIQSwsxlRQ0", + "q": "2jMFt2iFrdaYabdXuB4QMboVjPvbLA-IVb6_0hSG_-EueGBvgcBxdFGIZaG6kqHqlB7qMsSzdptU0vn6IgmCZnX-Hlt6c5X7JB_q91PZMLTO01pbZ2Bk58GloalCHnw_mjPh0YPviH5jGoWM5RHyl_HDDMI-UeLkzP7ImxGizrM" + } + ] + } } } + trust_sources = dynamic_trust_evaluators_loader(config) assert "mock" in trust_sources assert trust_sources["mock"].__class__.__name__ == "MockTrustEvaluator" From f6903a53d9be45baf46d73042e1b175062b0babe Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 24 Oct 2024 14:41:00 +0200 Subject: [PATCH 13/44] chore: added todo --- pyeudiw/trust/default/direct_trust_sd_jwt_vc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyeudiw/trust/default/direct_trust_sd_jwt_vc.py b/pyeudiw/trust/default/direct_trust_sd_jwt_vc.py index 35f9d922..3a631d8f 100644 --- a/pyeudiw/trust/default/direct_trust_sd_jwt_vc.py +++ b/pyeudiw/trust/default/direct_trust_sd_jwt_vc.py @@ -43,6 +43,8 @@ def __init__(self, httpc_params: Optional[dict] = None, cache_ttl: int = 0, jwk_ self.jwk_endpoint = jwk_endpoint self.metadata_endpoint = metadata_endpoint self._vci_jwks_source: VciJwksSource = None + + # TODO: remove the if statement below and integrate in an unique class that uses the cache and non-cache approach if self.cache_ttl == 0: self._vci_jwks_source = RemoteVciJwksSource(httpc_params, jwk_endpoint) else: @@ -68,6 +70,7 @@ def get_metadata(self, issuer: str) -> dict: """ if not issuer: raise ValueError("invalid issuer: cannot be empty value") + issuer_normalized = [issuer if issuer[-1] != '/' else issuer[:-1]] url = issuer_normalized + self.metadata_endpoint if self.cache_ttl == 0: From a98d782c60220aa1f01d2b07cacced12dd5161ae Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 24 Oct 2024 18:29:20 +0200 Subject: [PATCH 14/44] feat: implemented methods for trust source --- pyeudiw/storage/base_storage.py | 26 ++++++++++++++++++++++++++ pyeudiw/storage/db_engine.py | 6 ++++++ pyeudiw/storage/mongo_storage.py | 8 ++++++++ 3 files changed, 40 insertions(+) diff --git a/pyeudiw/storage/base_storage.py b/pyeudiw/storage/base_storage.py index 067a9e55..4cdb3526 100644 --- a/pyeudiw/storage/base_storage.py +++ b/pyeudiw/storage/base_storage.py @@ -205,6 +205,32 @@ def add_trust_attestation_metadata(self, entity_id: str, metadata_type: str, met :rtype: str """ raise NotImplementedError() + + def add_trust_source(self, entity_id: str, trust_source: dict) -> str: + """ + Add a trust source. + + :param entity_id: the entity id. + :type entity_id: str + :param trust_source: the trust source. + :type trust_source: dict + + :returns: the document id. + :rtype: str + """ + raise NotImplementedError() + + def get_trust_source(self, entity_id: str) -> Union[dict, None]: + """ + Get a trust source. + + :param entity_id: the entity id. + :type entity_id: str + + :returns: the trust source. + :rtype: Union[dict, None] + """ + raise NotImplementedError() def add_trust_anchor(self, entity_id: str, entity_configuration: str, exp: datetime, trust_type: TrustType): """ diff --git a/pyeudiw/storage/db_engine.py b/pyeudiw/storage/db_engine.py index 6b9411c0..7ae923bb 100644 --- a/pyeudiw/storage/db_engine.py +++ b/pyeudiw/storage/db_engine.py @@ -161,6 +161,12 @@ def add_trust_attestation(self, entity_id: str, attestation: list[str] = [], exp def add_trust_attestation_metadata(self, entity_id: str, metadat_type: str, metadata: dict) -> str: return self.write("add_trust_attestation_metadata", entity_id, metadat_type, metadata) + + def add_trust_source(self, entity_id: str, trust_source: dict) -> str: + return self.write("add_trust_source", entity_id, trust_source) + + def get_trust_source(self, entity_id: str) -> dict: + return self.get("get_trust_source", entity_id) def add_trust_anchor(self, entity_id: str, entity_configuration: str, exp: datetime, trust_type: TrustType = TrustType.FEDERATION) -> str: return self.write("add_trust_anchor", entity_id, entity_configuration, exp, trust_type) diff --git a/pyeudiw/storage/mongo_storage.py b/pyeudiw/storage/mongo_storage.py index 518d29e9..0a2389fc 100644 --- a/pyeudiw/storage/mongo_storage.py +++ b/pyeudiw/storage/mongo_storage.py @@ -298,6 +298,14 @@ def add_trust_attestation(self, entity_id: str, attestation: list[str], exp: dat return self._add_entry( "trust_attestations", entity_id, updated_entity, exp ) + + def add_trust_source(self, entity_id: str, trust_source: dict) -> str: + return self._add_entry( + "trust_source", entity_id, trust_source, trust_source["exp"] + ) + + def get_trust_source(self, entity_id: str) -> dict | None: + return self._get_trust_attestation("trust_source", entity_id) def add_trust_attestation_metadata(self, entity_id: str, metadata_type: str, metadata: dict): entity = self._get_trust_attestation("trust_attestations", entity_id) From ff51ca9d221f7e41c8bb258d523322a3bdc0af3f Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 24 Oct 2024 18:29:36 +0200 Subject: [PATCH 15/44] fix: method name --- pyeudiw/trust/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeudiw/trust/interface.py b/pyeudiw/trust/interface.py index 9b47a31a..466a6946 100644 --- a/pyeudiw/trust/interface.py +++ b/pyeudiw/trust/interface.py @@ -40,5 +40,5 @@ def is_revoked(self, issuer: str) -> bool: def get_policies(self, issuer: str) -> dict: raise NotImplementedError("reserved for future uses") - def get_protected_header_resource(self): + def get_selfissued_jwt_header_trust_parameters(self) -> dict: raise NotImplementedError From c98974b30bf82888dd5f609b9e3d9057c745139f Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 24 Oct 2024 18:32:21 +0200 Subject: [PATCH 16/44] chore: module initialization --- pyeudiw/trust/model/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 pyeudiw/trust/model/__init__.py diff --git a/pyeudiw/trust/model/__init__.py b/pyeudiw/trust/model/__init__.py new file mode 100644 index 00000000..e69de29b From 0c2e6de821ad3eed50761f221b46aac02461e7c2 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 24 Oct 2024 18:33:22 +0200 Subject: [PATCH 17/44] feat: implemented TrustSourceData model --- pyeudiw/trust/model/trust_source.py | 92 +++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 pyeudiw/trust/model/trust_source.py diff --git a/pyeudiw/trust/model/trust_source.py b/pyeudiw/trust/model/trust_source.py new file mode 100644 index 00000000..53ebf675 --- /dev/null +++ b/pyeudiw/trust/model/trust_source.py @@ -0,0 +1,92 @@ +from dataclasses import dataclass +from pyeudiw.jwk import JWK +from datetime import datetime + +@dataclass +class TrustParameterData: + def __init__(self, type: str, trust_params: dict, expiration_date: datetime) -> None: + self.type = type + self.trust_params = trust_params + self.expiration_date = expiration_date + + def selfissued_jwt_header_trust_parameters(self) -> dict: + return {self.type: self.trust_params} + + def serialize(self) -> dict: + return { + "type": self.type, + "trust_params": self.trust_params, + "expiration_date": self.expiration_date + } + + @property + def expired(self) -> bool: + return datetime.now() > self.expiration_date + +@dataclass +class TrustSourceData: + def __init__( + self, + client_id: str, + policies: dict = {}, + metadata: dict = {}, + revoked: bool = False, + keys: list[dict] = [], + trust_params: dict[str, dict[str, any]] = {} + ) -> None: + self.client_id = client_id + self.policies = policies + self.metadata = metadata + self.revoked = revoked + self.keys = keys + + self.trust_params = [TrustParameterData(**tp) for tp in trust_params] + + @property + def metadata(self) -> dict: + return self.metadata + + @property + def is_revoked(self) -> bool: + return self.revoked + + @property + def policies(self) -> dict: + return self.policies + + @property + def public_keys(self) -> list[dict]: + return [JWK(k).as_public_dict() for k in self.keys] + + def add_key(self, key: dict) -> None: + self.keys.append(key) + + def add_keys(self, keys: list[dict]) -> None: + self.keys.extend(keys) + + def add_trust_source(self, type: str, trust_params: TrustParameterData) -> None: + self.trust_params[type] = trust_params + + def has_trust_source(self, type: str) -> bool: + return type in self.trust_params + + def get_trust_source(self, type: str) -> TrustParameterData: + return TrustParameterData(type, self.trust_params[type]) + + def serialize(self) -> dict: + return { + "client_id": self.client_id, + "policies": self.policies, + "metadata": self.metadata, + "revoked": self.revoked, + "keys": self.keys, + "trust_params": [param.serialize() for param in self.trust_params] + } + + @staticmethod + def empty(client_id: str) -> 'TrustSourceData': + return TrustSourceData(client_id) + + @staticmethod + def from_dict(data: dict) -> 'TrustSourceData': + return TrustSourceData(**data) From f1d527c532608da5bcf712d3ca860557e9427cdd Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 24 Oct 2024 18:33:40 +0200 Subject: [PATCH 18/44] chore: initialized module --- pyeudiw/trust/handler/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 pyeudiw/trust/handler/__init__.py diff --git a/pyeudiw/trust/handler/__init__.py b/pyeudiw/trust/handler/__init__.py new file mode 100644 index 00000000..e69de29b From d4e443aeeac50e9ec4b3a50458022ee7cf4c85e2 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 24 Oct 2024 18:34:53 +0200 Subject: [PATCH 19/44] feat: implemented TrustHandlerInterface --- pyeudiw/trust/handler/interface.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 pyeudiw/trust/handler/interface.py diff --git a/pyeudiw/trust/handler/interface.py b/pyeudiw/trust/handler/interface.py new file mode 100644 index 00000000..bb7db351 --- /dev/null +++ b/pyeudiw/trust/handler/interface.py @@ -0,0 +1,20 @@ +from pyeudiw.trust.model.trust_source import TrustSourceData + +class TrustHandlerInterface: + @staticmethod + def extract( + self, + issuer: str, + trust_source: TrustSourceData, + data_endpoint: str, + httpc_params: dict + ) -> TrustSourceData: + NotImplementedError + + @staticmethod + def verify() -> bool: + NotImplementedError + + @staticmethod + def name() -> str: + NotImplementedError \ No newline at end of file From 15e33c1534d76669de60e4fbd3e9dc5642198688 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 24 Oct 2024 18:35:36 +0200 Subject: [PATCH 20/44] feat: initial implementation of metadata handler --- pyeudiw/trust/handler/metadata.py | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 pyeudiw/trust/handler/metadata.py diff --git a/pyeudiw/trust/handler/metadata.py b/pyeudiw/trust/handler/metadata.py new file mode 100644 index 00000000..13024068 --- /dev/null +++ b/pyeudiw/trust/handler/metadata.py @@ -0,0 +1,42 @@ +import os +from pyeudiw.tools.base_logger import BaseLogger +from pyeudiw.trust.model.trust_source import TrustSourceData +from pyeudiw.tools.utils import get_http_url +from pyeudiw.trust.handler.interface import TrustHandlerInterface + +DEAFAULT_METADATA_ENDPOINT = "/.well-known/openid-credential-issuer" +DEFAULT_DIRECT_TRUST_SD_JWC_VC_PARAMS = { + "httpc_params": { + "connection": { + "ssl": os.getenv("PYEUDIW_HTTPC_SSL", True) + }, + "session": { + "timeout": os.getenv("PYEUDIW_HTTPC_TIMEOUT", 6) + } + } +} + +class MetadataExtractor(TrustHandlerInterface, BaseLogger): + @staticmethod + def extract( + self, + issuer: str, + trust_source: TrustSourceData, + data_endpoint: str = DEAFAULT_METADATA_ENDPOINT, + httpc_params: dict = {} + ) -> TrustSourceData: + issuer_normalized = [issuer if issuer[-1] != '/' else issuer[:-1]] + url = issuer_normalized + data_endpoint + + try: + response = get_http_url(url, httpc_params) + metadata = response[0].json() + trust_source.metadata = metadata + return trust_source + except Exception as e: + self._log_warning("Metadata Extraction", f"error fetching metadata from {url}: {e}") + return trust_source + + @staticmethod + def name() -> str: + return "MetadataExtractor" \ No newline at end of file From 12743d8296ceb54a1cc561c36e713caec4cf1fce Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 24 Oct 2024 18:36:20 +0200 Subject: [PATCH 21/44] fix: class name --- pyeudiw/trust/handler/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeudiw/trust/handler/metadata.py b/pyeudiw/trust/handler/metadata.py index 13024068..5175048b 100644 --- a/pyeudiw/trust/handler/metadata.py +++ b/pyeudiw/trust/handler/metadata.py @@ -16,7 +16,7 @@ } } -class MetadataExtractor(TrustHandlerInterface, BaseLogger): +class MetadataHandler(TrustHandlerInterface, BaseLogger): @staticmethod def extract( self, From 16ea0affd6eb7f1012700bd02378eed9a45bb60f Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 24 Oct 2024 18:37:18 +0200 Subject: [PATCH 22/44] feat: initial implementation of DirectTrustJWTHandler --- .../trust/handler/direct_trust_sd_jwt_vc.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 pyeudiw/trust/handler/direct_trust_sd_jwt_vc.py diff --git a/pyeudiw/trust/handler/direct_trust_sd_jwt_vc.py b/pyeudiw/trust/handler/direct_trust_sd_jwt_vc.py new file mode 100644 index 00000000..bb9b361f --- /dev/null +++ b/pyeudiw/trust/handler/direct_trust_sd_jwt_vc.py @@ -0,0 +1,42 @@ +import os +from pyeudiw.trust.handler.interface import TrustHandlerInterface +from pyeudiw.trust.model.trust_source import TrustSourceData +from pyeudiw.vci.jwks_provider import RemoteVciJwksSource +from pyeudiw.tools.base_logger import BaseLogger + + +DEAFAULT_JWK_ENDPOINT = "/.well-known/jwt-vc-issuer" + +DEFAULT_DIRECT_TRUST_SD_JWC_VC_PARAMS = { + "httpc_params": { + "connection": { + "ssl": os.getenv("PYEUDIW_HTTPC_SSL", True) + }, + "session": { + "timeout": os.getenv("PYEUDIW_HTTPC_TIMEOUT", 6) + } + } +} + +class DirectTrustJWTHandler(TrustHandlerInterface, BaseLogger): + @staticmethod + def extract( + self, + issuer: str, + trust_source: TrustSourceData, + data_endpoint: str = DEAFAULT_JWK_ENDPOINT, + httpc_params: dict = {} + ) -> TrustSourceData: + + try: + jwk_source = RemoteVciJwksSource(httpc_params, data_endpoint) + jwks = jwk_source.get_jwks(issuer) + trust_source.add_keys(jwks) + return trust_source + except Exception as e: + self._log_warning("JWK Extraction", f"error fetching JWK from {issuer}: {e}") + return trust_source + + @staticmethod + def name() -> str: + return "DirectTrustJWTExtractor" \ No newline at end of file From 643bc8d12567e2be59f4ef3ea49f4084ae4aaed5 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 24 Oct 2024 18:37:49 +0200 Subject: [PATCH 23/44] feat: refactoring of CombinedTrustEvaluator --- pyeudiw/trust/dynamic.py | 95 ++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/pyeudiw/trust/dynamic.py b/pyeudiw/trust/dynamic.py index 6da6eaf0..cac5c0a3 100644 --- a/pyeudiw/trust/dynamic.py +++ b/pyeudiw/trust/dynamic.py @@ -6,15 +6,15 @@ else: from typing_extensions import TypedDict -from pyeudiw.storage.base_storage import TrustType from pyeudiw.storage.db_engine import DBEngine from pyeudiw.tools.base_logger import BaseLogger -from pyeudiw.tools.utils import get_dynamic_class, satisfy_interface from pyeudiw.trust.default import default_trust_evaluator from pyeudiw.trust.exceptions import TrustConfigurationError from pyeudiw.trust.interface import TrustEvaluator from pyeudiw.trust._log import _package_logger -from pyeudiw.tools.utils import dynamic_class_loader +from pyeudiw.tools.utils import dynamic_class_loader, satisfy_interface +from pyeudiw.trust.handler.interface import TrustHandlerInterface +from pyeudiw.trust.model.trust_source import TrustSourceData TrustModuleConfiguration_T = TypedDict("_DynamicTrustConfiguration", {"module": str, "class": str, "config": dict}) @@ -51,17 +51,36 @@ def dynamic_trust_evaluators_loader(trust_config: dict[str, TrustModuleConfigura class CombinedTrustEvaluator(TrustEvaluator, BaseLogger): - """ - CombinedTrustEvaluator is a wrapper around multiple implementations of - TrustEvaluator. It's primary purpose is to handle how multiple configured - trust sources are queried when some metadata or key material is requested. - """ - - def __init__(self, trust_evaluators: dict[str, TrustEvaluator], client_id: str): - self.trust_evaluators: dict[str, TrustEvaluator] = trust_evaluators + def __init__( + self, + db_engine: DBEngine, + extracors: list[TrustHandlerInterface] + ) -> None: + self.db_engine: DBEngine = db_engine + self.extractors: list[TrustHandlerInterface] = extracors + self.extractors_names: list[str] = [e.name() for e in self.extractors] + + def _retrieve_trust_source(self, issuer: str) -> Optional[TrustSourceData]: + trust_source = self.db_engine.get_trust_source(issuer) + if trust_source: + return TrustSourceData.from_dict(trust_source) + return None + + def _extract_trust_source(self, issuer: str) -> Optional[TrustSourceData]: + trust_source = TrustSourceData.empty() + + for extractor in self.extractors: + trust_source: TrustSourceData = extractor.extract(issuer, trust_source) + + self.db_engine.add_trust_source(issuer, trust_source.serialize()) - def _get_trust_identifier_names(self) -> str: - return f'[{",".join(self.trust_evaluators.keys())}]' + return trust_source + + def _get_trust_source(self, issuer: str) -> TrustSourceData: + trust_source = self._retrieve_trust_source(issuer) + if not trust_source: + trust_source = self._extract_trust_source(issuer) + return trust_source def get_public_keys(self, issuer: str) -> list[dict]: """ @@ -70,38 +89,46 @@ def get_public_keys(self, issuer: str) -> list[dict]: :returns: a list of jwk(s); note that those key are _not_ necessarely identified by a kid claim """ - pks: list[dict] = [] - for eval_identifier, eval_instance in self.trust_evaluators.items(): - try: - new_pks = eval_instance.get_public_keys(issuer) - except Exception as e: - self._log_warning(f"failed to find any key of issuer {issuer} with model {eval_identifier}: {eval_instance.__class__.__name__}", e) - continue - if new_pks: - pks.extend(new_pks) - if not pks: - raise Exception(f"no trust evaluator can provide cyptographic material for {issuer}: searched among: {self._get_trust_identifier_names()}") - return pks + trust_source = self._get_trust_source(issuer) + + if not trust_source.keys: + raise Exception(f"no trust evaluator can provide cyptographic material for {issuer}: searched among: {self.extractors_names}") + + return trust_source.public_keys def get_metadata(self, issuer: str) -> dict: """ yields a dictionary of metadata about an issuer, according to some trust model. """ - md: dict = {} - for instance in self.trust_evaluators.values(): - md = instance.get_metadata(issuer) - if md: - return md - if not md: - raise Exception(f"no trust evaluator can provide metadata for {issuer}: searched among: {self._get_trust_identifier_names()}") + trust_source = self._get_trust_source(issuer) + + if not trust_source.metadata: + raise Exception(f"no trust evaluator can provide metadata for {issuer}: searched among: {self.extractors_names}") + + return trust_source.metadata def is_revoked(self, issuer: str) -> bool: """ yield if the trust toward the issuer was revoked according to some trust model; this asusmed that the isser exists, is valid, but is not trusted. """ - raise NotImplementedError("implementation details yet to be deifined for combined use") + trust_source = self._get_trust_source(issuer) + return trust_source.is_revoked def get_policies(self, issuer: str) -> dict: - raise NotImplementedError("reserved for future uses") + trust_source = self._get_trust_source(issuer) + + if not trust_source.policies: + raise Exception(f"no trust evaluator can provide policies for {issuer}: searched among: {self.extractors_names}") + + return trust_source.policies + + def get_selfissued_jwt_header_trust_parameters(self, issuer: str) -> dict: + trust_source = self._get_trust_source(issuer) + + if not trust_source.trust_params: + raise Exception(f"no trust evaluator can provide trust parameters for {issuer}: searched among: {self.extractors_names}") + + return trust_source.trust_params + From f4bf8865a6f7d4400e083f88e2203912c542c6e5 Mon Sep 17 00:00:00 2001 From: Pasquale De Rose Date: Thu, 24 Oct 2024 18:39:28 +0200 Subject: [PATCH 24/44] Update pyeudiw/jwt/verification.py Co-authored-by: Giuseppe De Marco --- pyeudiw/jwt/verification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyeudiw/jwt/verification.py b/pyeudiw/jwt/verification.py index 8d6be6e6..afa6a71d 100644 --- a/pyeudiw/jwt/verification.py +++ b/pyeudiw/jwt/verification.py @@ -30,7 +30,7 @@ def is_jwt_expired(token: str) -> bool: exp = token_payload.get("exp", None) if not exp: return True - if exp < iat_now(): + elif exp < iat_now(): return True return False From 59328bae139efbaa7888a55c6f401a758d65d838 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:20:46 +0200 Subject: [PATCH 25/44] feat: added trust source methods and refactoring --- pyeudiw/storage/base_storage.py | 3 ++ pyeudiw/storage/db_engine.py | 3 ++ pyeudiw/storage/mongo_storage.py | 65 +++++++++++++++++++++----------- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/pyeudiw/storage/base_storage.py b/pyeudiw/storage/base_storage.py index 4cdb3526..89423cfa 100644 --- a/pyeudiw/storage/base_storage.py +++ b/pyeudiw/storage/base_storage.py @@ -171,6 +171,9 @@ def has_trust_anchor(self, entity_id: str) -> bool: :rtype: bool """ raise NotImplementedError() + + def has_trust_source(self, entity_id: str) -> bool: + raise NotImplementedError() def add_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType, jwks: dict) -> str: """ diff --git a/pyeudiw/storage/db_engine.py b/pyeudiw/storage/db_engine.py index 7ae923bb..6141291a 100644 --- a/pyeudiw/storage/db_engine.py +++ b/pyeudiw/storage/db_engine.py @@ -155,6 +155,9 @@ def has_trust_attestation(self, entity_id: str) -> bool: def has_trust_anchor(self, entity_id: str) -> bool: return self.get_trust_anchor(entity_id) is not None + + def has_trust_source(self, entity_id: str) -> bool: + return self.get_trust_source(entity_id) is not None def add_trust_attestation(self, entity_id: str, attestation: list[str] = [], exp: datetime = None, trust_type: TrustType = TrustType.FEDERATION, jwks: list[dict] = []) -> str: return self.write("add_trust_attestation", entity_id, attestation, exp, trust_type, jwks) diff --git a/pyeudiw/storage/mongo_storage.py b/pyeudiw/storage/mongo_storage.py index 0a2389fc..0b93481e 100644 --- a/pyeudiw/storage/mongo_storage.py +++ b/pyeudiw/storage/mongo_storage.py @@ -57,6 +57,9 @@ def _connect(self): self.trust_anchors = getattr( self.db, self.storage_conf["db_trust_anchors_collection"] ) + self.trust_sources = getattr( + self.db, self.storage_conf["db_trust_sources_collection"] + ) def close(self): self._connect() @@ -209,25 +212,48 @@ def update_response_object(self, nonce: str, state: str, internal_response: dict return document_status - def _get_trust_attestation(self, collection: str, entity_id: str) -> dict | None: + def _get_db_entity(self, collection: str, entity_id: str) -> dict | None: self._connect() db_collection = getattr(self, collection) return db_collection.find_one({"entity_id": entity_id}) + + def get_trust_source(self, entity_id: str) -> dict | None: + return self._get_db_entity( + self.storage_conf["db_trust_sources_collection"], entity_id + ) def get_trust_attestation(self, entity_id: str) -> dict | None: - return self._get_trust_attestation("trust_attestations", entity_id) + return self._get_db_entity( + self.storage_conf["db_trust_attestations_collection"], + entity_id + ) def get_trust_anchor(self, entity_id: str) -> dict | None: - return self._get_trust_attestation("trust_anchors", entity_id) + return self._get_db_entity( + self.storage_conf["db_trust_anchors_collection"], + entity_id + ) - def _has_trust_attestation(self, collection: str, entity_id: str) -> bool: - return self._get_trust_attestation(collection, entity_id) is not None + def _has_db_entity(self, collection: str, entity_id: str) -> bool: + return self._get_db_entity(collection, entity_id) is not None def has_trust_attestation(self, entity_id: str) -> bool: - return self._has_trust_attestation("trust_attestations", entity_id) + return self._has_db_entity( + self.storage_conf["db_trust_attestations_collection"], + entity_id + ) def has_trust_anchor(self, entity_id: str) -> bool: - return self._has_trust_attestation("trust_anchors", entity_id) + return self._has_db_entity( + self.storage_conf["db_trust_anchors_collection"], + entity_id + ) + + def has_trust_source(self, entity_id: str) -> bool: + return self._has_db_entity( + self.storage_conf["db_trust_sources_collection"], + entity_id + ) def _add_entry( self, @@ -296,19 +322,16 @@ def add_trust_attestation(self, entity_id: str, attestation: list[str], exp: dat entity, attestation, exp, trust_type, jwks) return self._add_entry( - "trust_attestations", entity_id, updated_entity, exp + self.storage_conf["db_trust_attestations_collection"], entity_id, updated_entity, exp ) def add_trust_source(self, entity_id: str, trust_source: dict) -> str: return self._add_entry( - "trust_source", entity_id, trust_source, trust_source["exp"] + "trust_sources", entity_id, trust_source, trust_source.get("exp") ) - - def get_trust_source(self, entity_id: str) -> dict | None: - return self._get_trust_attestation("trust_source", entity_id) def add_trust_attestation_metadata(self, entity_id: str, metadata_type: str, metadata: dict): - entity = self._get_trust_attestation("trust_attestations", entity_id) + entity = self._get_db_entity(self.storage_conf["db_trust_attestations_collection"], entity_id) if entity is None: raise ValueError( @@ -317,7 +340,7 @@ def add_trust_attestation_metadata(self, entity_id: str, metadata_type: str, met entity["metadata"][metadata_type] = metadata - return self._update_trust_attestation("trust_attestations", entity_id, entity) + return self._update_trust_attestation(self.storage_conf["db_trust_attestations_collection"], entity_id, entity) def add_trust_anchor(self, entity_id: str, entity_configuration: str, exp: datetime, trust_type: TrustType): if self.has_trust_anchor(entity_id): @@ -331,10 +354,10 @@ def add_trust_anchor(self, entity_id: str, entity_configuration: str, exp: datet updated_entity = self._update_anchor_metadata( entity, entity_configuration, exp, trust_type) - return self._add_entry("trust_anchors", entity_id, updated_entity, exp) + return self._add_entry(self.storage_conf["db_trust_anchors_collection"], entity_id, updated_entity, exp) def _update_trust_attestation(self, collection: str, entity_id: str, entity: dict) -> str: - if not self._has_trust_attestation(collection, entity_id): + if not self._has_db_entity(collection, entity_id): raise ChainNotExist(f"Chain with entity id {entity_id} not exist") documentStatus = self.trust_attestations.update_one( @@ -344,16 +367,16 @@ def _update_trust_attestation(self, collection: str, entity_id: str, entity: dic return documentStatus def update_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType, jwks: list[dict]) -> str: - old_entity = self._get_trust_attestation( - "trust_attestations", entity_id) or {} + old_entity = self._get_db_entity( + self.storage_conf["db_trust_attestations_collection"], entity_id) or {} upd_entity = self._update_attestation_metadata( old_entity, attestation, exp, trust_type, jwks) - return self._update_trust_attestation("trust_attestations", entity_id, upd_entity) + return self._update_trust_attestation(self.storage_conf["db_trust_attestations_collection"], entity_id, upd_entity) def update_trust_anchor(self, entity_id: str, entity_configuration: str, exp: datetime, trust_type: TrustType) -> str: - old_entity = self._get_trust_attestation( - "trust_attestations", entity_id) or {} + old_entity = self._get_db_entity( + self.storage_conf["db_trust_attestations_collection"], entity_id) or {} upd_entity = self._update_anchor_metadata( old_entity, entity_configuration, exp, trust_type) From 31604c965f23b06e7f1b91c011e827d9875c7246 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:23:56 +0200 Subject: [PATCH 26/44] feat: merged implementations --- .../trust/handler/direct_trust_sd_jwt_vc.py | 39 +++++++++++------ pyeudiw/trust/handler/metadata.py | 42 ------------------- 2 files changed, 26 insertions(+), 55 deletions(-) delete mode 100644 pyeudiw/trust/handler/metadata.py diff --git a/pyeudiw/trust/handler/direct_trust_sd_jwt_vc.py b/pyeudiw/trust/handler/direct_trust_sd_jwt_vc.py index bb9b361f..bc7c9e48 100644 --- a/pyeudiw/trust/handler/direct_trust_sd_jwt_vc.py +++ b/pyeudiw/trust/handler/direct_trust_sd_jwt_vc.py @@ -3,9 +3,11 @@ from pyeudiw.trust.model.trust_source import TrustSourceData from pyeudiw.vci.jwks_provider import RemoteVciJwksSource from pyeudiw.tools.base_logger import BaseLogger +from pyeudiw.tools.utils import get_http_url DEAFAULT_JWK_ENDPOINT = "/.well-known/jwt-vc-issuer" +DEAFAULT_METADATA_ENDPOINT = "/.well-known/openid-credential-issuer" DEFAULT_DIRECT_TRUST_SD_JWC_VC_PARAMS = { "httpc_params": { @@ -19,24 +21,35 @@ } class DirectTrustJWTHandler(TrustHandlerInterface, BaseLogger): - @staticmethod - def extract( + def __init__( self, - issuer: str, - trust_source: TrustSourceData, - data_endpoint: str = DEAFAULT_JWK_ENDPOINT, - httpc_params: dict = {} - ) -> TrustSourceData: - + httpc_params: dict = DEFAULT_DIRECT_TRUST_SD_JWC_VC_PARAMS, + jwk_endpoint: str = DEAFAULT_JWK_ENDPOINT, + metadata_endpoint: str = DEAFAULT_METADATA_ENDPOINT + ) -> None: + self.httpc_params = httpc_params + self.jwk_endpoint = jwk_endpoint + self.metadata_endpoint = metadata_endpoint + + def extract(self, issuer: str, trust_source: TrustSourceData) -> TrustSourceData: try: - jwk_source = RemoteVciJwksSource(httpc_params, data_endpoint) + self.get_metadata(issuer, trust_source) + jwk_source = RemoteVciJwksSource(self.httpc_params, self.jwk_endpoint) jwks = jwk_source.get_jwks(issuer) trust_source.add_keys(jwks) return trust_source except Exception as e: self._log_warning("JWK Extraction", f"error fetching JWK from {issuer}: {e}") return trust_source - - @staticmethod - def name() -> str: - return "DirectTrustJWTExtractor" \ No newline at end of file + + def get_metadata(self, issuer: str, trust_source: TrustSourceData) -> TrustSourceData: + issuer_normalized = [issuer if issuer[-1] != '/' else issuer[:-1]] + url = issuer_normalized + self.metadata_endpoint + + try: + response = get_http_url(url, self.httpc_params) + metadata = response[0].json() + trust_source.metadata = metadata + return trust_source + except Exception as e: + self._log_warning("Metadata Extraction", f"error fetching metadata from {url}: {e}") \ No newline at end of file diff --git a/pyeudiw/trust/handler/metadata.py b/pyeudiw/trust/handler/metadata.py deleted file mode 100644 index 5175048b..00000000 --- a/pyeudiw/trust/handler/metadata.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -from pyeudiw.tools.base_logger import BaseLogger -from pyeudiw.trust.model.trust_source import TrustSourceData -from pyeudiw.tools.utils import get_http_url -from pyeudiw.trust.handler.interface import TrustHandlerInterface - -DEAFAULT_METADATA_ENDPOINT = "/.well-known/openid-credential-issuer" -DEFAULT_DIRECT_TRUST_SD_JWC_VC_PARAMS = { - "httpc_params": { - "connection": { - "ssl": os.getenv("PYEUDIW_HTTPC_SSL", True) - }, - "session": { - "timeout": os.getenv("PYEUDIW_HTTPC_TIMEOUT", 6) - } - } -} - -class MetadataHandler(TrustHandlerInterface, BaseLogger): - @staticmethod - def extract( - self, - issuer: str, - trust_source: TrustSourceData, - data_endpoint: str = DEAFAULT_METADATA_ENDPOINT, - httpc_params: dict = {} - ) -> TrustSourceData: - issuer_normalized = [issuer if issuer[-1] != '/' else issuer[:-1]] - url = issuer_normalized + data_endpoint - - try: - response = get_http_url(url, httpc_params) - metadata = response[0].json() - trust_source.metadata = metadata - return trust_source - except Exception as e: - self._log_warning("Metadata Extraction", f"error fetching metadata from {url}: {e}") - return trust_source - - @staticmethod - def name() -> str: - return "MetadataExtractor" \ No newline at end of file From 0f2c70d4c1bceca305eedd69912950827d5e777b Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:26:00 +0200 Subject: [PATCH 27/44] chore: moved type --- pyeudiw/satosa/schemas/config.py | 2 +- pyeudiw/trust/dynamic.py | 6 ------ pyeudiw/trust/model/__init__.py | 7 +++++++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pyeudiw/satosa/schemas/config.py b/pyeudiw/satosa/schemas/config.py index be8f1b2d..b66b18c2 100644 --- a/pyeudiw/satosa/schemas/config.py +++ b/pyeudiw/satosa/schemas/config.py @@ -9,7 +9,7 @@ from pyeudiw.satosa.schemas.user_attributes import UserAttributesConfig from pyeudiw.satosa.schemas.ui import UiConfig from pyeudiw.storage.schemas.storage import Storage -from pyeudiw.trust.dynamic import TrustModuleConfiguration_T +from pyeudiw.trust.model import TrustModuleConfiguration_T class PyeudiwBackendConfig(BaseModel): diff --git a/pyeudiw/trust/dynamic.py b/pyeudiw/trust/dynamic.py index cac5c0a3..8a69d49e 100644 --- a/pyeudiw/trust/dynamic.py +++ b/pyeudiw/trust/dynamic.py @@ -1,11 +1,5 @@ import sys from typing import Optional - -if float(f"{sys.version_info.major}.{sys.version_info.minor}") >= 3.12: - from typing import TypedDict -else: - from typing_extensions import TypedDict - from pyeudiw.storage.db_engine import DBEngine from pyeudiw.tools.base_logger import BaseLogger from pyeudiw.trust.default import default_trust_evaluator diff --git a/pyeudiw/trust/model/__init__.py b/pyeudiw/trust/model/__init__.py index e69de29b..a943b7f3 100644 --- a/pyeudiw/trust/model/__init__.py +++ b/pyeudiw/trust/model/__init__.py @@ -0,0 +1,7 @@ +import sys +if float(f"{sys.version_info.major}.{sys.version_info.minor}") >= 3.12: + from typing import TypedDict +else: + from typing_extensions import TypedDict + +TrustModuleConfiguration_T = TypedDict("_DynamicTrustConfiguration", {"module": str, "class": str, "config": dict}) \ No newline at end of file From 19d9a3339a0bcfa24ec6569b74a16244664cc5db Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:26:57 +0200 Subject: [PATCH 28/44] chore: updated config --- pyeudiw/tests/settings.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyeudiw/tests/settings.py b/pyeudiw/tests/settings.py index 541fe83d..abc80065 100644 --- a/pyeudiw/tests/settings.py +++ b/pyeudiw/tests/settings.py @@ -61,8 +61,8 @@ }, "trust": { "direct_trust_sd_jwt_vc": { - "module": "pyeudiw.trust.default.direct_trust_sd_jwt_vc", - "class": "DirectTrustSdJwtVc", + "module": "pyeudiw.trust.handler.direct_trust_sd_jwt_vc", + "class": "DirectTrustJWTHandler", "config": { "jwk_endpoint": "/.well-known/jwt-vc-issuer", "httpc_params": { @@ -76,8 +76,8 @@ } }, "federation": { - "module": "pyeudiw.trust.default.federation", - "class": "FederationTrustModel", + "module": "pyeudiw.trust.handler.federation", + "class": "FederationHandler", "config": { "metadata_type": "wallet_relying_party", "authority_hints": [ @@ -173,7 +173,8 @@ "db_name": "test-eudiw", "db_sessions_collection": "sessions", "db_trust_attestations_collection": "trust_attestations", - "db_trust_anchors_collection": "trust_anchors" + "db_trust_anchors_collection": "trust_anchors", + "db_trust_sources_collection": "trust_sources" }, "connection_params": {} } From d1cbf2926a20d36b6f489f0c39289ffb37bf4d41 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:27:24 +0200 Subject: [PATCH 29/44] fix: update mongo tests --- pyeudiw/tests/storage/test_mongo_storage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyeudiw/tests/storage/test_mongo_storage.py b/pyeudiw/tests/storage/test_mongo_storage.py index d1f02149..f1b7706f 100644 --- a/pyeudiw/tests/storage/test_mongo_storage.py +++ b/pyeudiw/tests/storage/test_mongo_storage.py @@ -13,7 +13,8 @@ def create_storage_instance(self): "db_name": "test-eudiw", "db_sessions_collection": "sessions", "db_trust_attestations_collection": "trust_attestations", - "db_trust_anchors_collection": "trust_anchors" + "db_trust_anchors_collection": "trust_anchors", + "db_trust_sources_collection": "trust_source" }, "mongodb://localhost:27017/", {} From 29ad2b54413b96fdb53abf2d0bc07450b6f81c2b Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:27:51 +0200 Subject: [PATCH 30/44] fix: fixed model --- pyeudiw/trust/model/trust_source.py | 37 ++++++++++++----------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/pyeudiw/trust/model/trust_source.py b/pyeudiw/trust/model/trust_source.py index 53ebf675..99649d84 100644 --- a/pyeudiw/trust/model/trust_source.py +++ b/pyeudiw/trust/model/trust_source.py @@ -4,7 +4,12 @@ @dataclass class TrustParameterData: - def __init__(self, type: str, trust_params: dict, expiration_date: datetime) -> None: + def __init__( + self, + type: str, + trust_params: dict, + expiration_date: datetime, + ) -> None: self.type = type self.trust_params = trust_params self.expiration_date = expiration_date @@ -27,36 +32,20 @@ def expired(self) -> bool: class TrustSourceData: def __init__( self, - client_id: str, + entity_id: str, policies: dict = {}, metadata: dict = {}, revoked: bool = False, keys: list[dict] = [], trust_params: dict[str, dict[str, any]] = {} ) -> None: - self.client_id = client_id + self.entity_id = entity_id self.policies = policies self.metadata = metadata self.revoked = revoked self.keys = keys self.trust_params = [TrustParameterData(**tp) for tp in trust_params] - - @property - def metadata(self) -> dict: - return self.metadata - - @property - def is_revoked(self) -> bool: - return self.revoked - - @property - def policies(self) -> dict: - return self.policies - - @property - def public_keys(self) -> list[dict]: - return [JWK(k).as_public_dict() for k in self.keys] def add_key(self, key: dict) -> None: self.keys.append(key) @@ -75,7 +64,7 @@ def get_trust_source(self, type: str) -> TrustParameterData: def serialize(self) -> dict: return { - "client_id": self.client_id, + "entity_id": self.entity_id, "policies": self.policies, "metadata": self.metadata, "revoked": self.revoked, @@ -84,9 +73,13 @@ def serialize(self) -> dict: } @staticmethod - def empty(client_id: str) -> 'TrustSourceData': - return TrustSourceData(client_id) + def empty(entity_id: str) -> 'TrustSourceData': + return TrustSourceData(entity_id) @staticmethod def from_dict(data: dict) -> 'TrustSourceData': return TrustSourceData(**data) + + @property + def public_keys(self) -> list[dict]: + return [JWK(k).as_public_dict() for k in self.keys] From b66c44e7a66f2f542fd685dcd672a20216a13a81 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:28:17 +0200 Subject: [PATCH 31/44] chore: removed unused code --- pyeudiw/trust/_log.py | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 pyeudiw/trust/_log.py diff --git a/pyeudiw/trust/_log.py b/pyeudiw/trust/_log.py deleted file mode 100644 index c5c1e1a3..00000000 --- a/pyeudiw/trust/_log.py +++ /dev/null @@ -1,4 +0,0 @@ -import logging - - -_package_logger = logging.getLogger(__name__) From 30b5fced3880878c9f20f5402599a7e8904be0c4 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:28:38 +0200 Subject: [PATCH 32/44] chore: removed unused function --- pyeudiw/tools/utils.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/pyeudiw/tools/utils.py b/pyeudiw/tools/utils.py index bc8ff1d7..f015f047 100644 --- a/pyeudiw/tools/utils.py +++ b/pyeudiw/tools/utils.py @@ -177,24 +177,3 @@ def dynamic_class_loader(module_name: str, class_name: str, init_params: dict = storage_instance = get_dynamic_class( module_name, class_name)(**init_params) return storage_instance - - -def satisfy_interface(o: object, interface: type) -> bool: - """ - Returns true if and only if an object satisfy an interface. - - :param o: an object (instance of a class) - :type o: object - :param interface: an interface type - :type interface: type - - :returns: True if the object satisfy the interface, otherwise False - """ - for cls_attr in dir(interface): - if cls_attr.startswith('_'): - continue - if not hasattr(o, cls_attr): - return False - if callable(getattr(interface, cls_attr)) and not callable(getattr(o, cls_attr)): - return False - return True From 1fd94170c6d9dd379223be593fd850175cf9a253 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:29:13 +0200 Subject: [PATCH 33/44] chore: string interpolation --- pyeudiw/trust/default/direct_trust_sd_jwt_vc.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyeudiw/trust/default/direct_trust_sd_jwt_vc.py b/pyeudiw/trust/default/direct_trust_sd_jwt_vc.py index 3a631d8f..f45cd775 100644 --- a/pyeudiw/trust/default/direct_trust_sd_jwt_vc.py +++ b/pyeudiw/trust/default/direct_trust_sd_jwt_vc.py @@ -79,9 +79,11 @@ def get_metadata(self, issuer: str) -> dict: return cacheable_get_http_url(ttl_timestamp, url, self.httpc_params)[0].json() def __str__(self) -> str: - return f"DirectTrustSdJwtVc(" \ - f"httpc_params={self.httpc_params}, " \ - f"cache_ttl={self.cache_ttl}, " \ - f"jwk_endpoint={self.jwk_endpoint}, " \ - f"metadata_endpoint={self.metadata_endpoint}" \ + return ( + f"DirectTrustSdJwtVc(" + f"httpc_params={self.httpc_params}, " + f"cache_ttl={self.cache_ttl}, " + f"jwk_endpoint={self.jwk_endpoint}, " + f"metadata_endpoint={self.metadata_endpoint}" ")" + ) From 4a28df193113ca32715f320d9a675793fa26db4f Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:30:43 +0200 Subject: [PATCH 34/44] feat: dinamyc backend refactoring --- pyeudiw/trust/dynamic.py | 98 ++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 53 deletions(-) diff --git a/pyeudiw/trust/dynamic.py b/pyeudiw/trust/dynamic.py index 8a69d49e..61027c13 100644 --- a/pyeudiw/trust/dynamic.py +++ b/pyeudiw/trust/dynamic.py @@ -1,69 +1,34 @@ -import sys +import logging from typing import Optional from pyeudiw.storage.db_engine import DBEngine from pyeudiw.tools.base_logger import BaseLogger -from pyeudiw.trust.default import default_trust_evaluator from pyeudiw.trust.exceptions import TrustConfigurationError from pyeudiw.trust.interface import TrustEvaluator -from pyeudiw.trust._log import _package_logger -from pyeudiw.tools.utils import dynamic_class_loader, satisfy_interface +from pyeudiw.tools.utils import dynamic_class_loader from pyeudiw.trust.handler.interface import TrustHandlerInterface from pyeudiw.trust.model.trust_source import TrustSourceData +from pyeudiw.trust.handler.direct_trust_sd_jwt_vc import DirectTrustJWTHandler +from pyeudiw.storage.exceptions import EntryNotFound - -TrustModuleConfiguration_T = TypedDict("_DynamicTrustConfiguration", {"module": str, "class": str, "config": dict}) - - -def dynamic_trust_evaluators_loader(trust_config: dict[str, TrustModuleConfiguration_T]) -> dict[str, TrustEvaluator]: # type: ignore - """Load a dynamically importable/configurable set of TrustEvaluators, - identified by the trust model they refer to. - If not configurations a re given, a default is returned instead - implementation of TrustEvaluator is returned instead. - - :return: a dictionary where the keys are common name identifiers - for the trust mechanism ,a nd the keys are acqual class instances that satisfy - the TrustEvaluator interface - :rtype: dict[str, TrustEvaluator] - """ - trust_instances: dict[str, TrustEvaluator] = {} - if not trust_config: - _package_logger.warning("no configured trust model, using direct trust model") - trust_instances["direct_trust_sd_jwt_vc"] = default_trust_evaluator() - return trust_instances - - for trust_model_name, trust_module_config in trust_config.items(): - try: - trust_evaluator_instance = dynamic_class_loader(trust_module_config["module"], trust_module_config["class"], trust_module_config["config"]) - except Exception as e: - raise TrustConfigurationError(f"invalid configuration for {trust_model_name}: {e}", e) - - if not satisfy_interface(trust_evaluator_instance, TrustEvaluator): - raise TrustConfigurationError(f"class {trust_evaluator_instance.__class__} does not satisfy the interface TrustEvaluator") - - trust_instances[trust_model_name] = trust_evaluator_instance - return trust_instances - +logger = logging.getLogger(__name__) class CombinedTrustEvaluator(TrustEvaluator, BaseLogger): - def __init__( - self, - db_engine: DBEngine, - extracors: list[TrustHandlerInterface] - ) -> None: + def __init__(self, handlers: list[TrustHandlerInterface], db_engine: DBEngine) -> None: self.db_engine: DBEngine = db_engine - self.extractors: list[TrustHandlerInterface] = extracors - self.extractors_names: list[str] = [e.name() for e in self.extractors] + self.handlers: list[TrustHandlerInterface] = handlers + self.handlers_names: list[str] = [e.name for e in self.handlers] def _retrieve_trust_source(self, issuer: str) -> Optional[TrustSourceData]: - trust_source = self.db_engine.get_trust_source(issuer) - if trust_source: + try: + trust_source = self.db_engine.get_trust_source(issuer) return TrustSourceData.from_dict(trust_source) - return None + except EntryNotFound: + return None def _extract_trust_source(self, issuer: str) -> Optional[TrustSourceData]: - trust_source = TrustSourceData.empty() + trust_source = TrustSourceData.empty(issuer) - for extractor in self.extractors: + for extractor in self.handlers: trust_source: TrustSourceData = extractor.extract(issuer, trust_source) self.db_engine.add_trust_source(issuer, trust_source.serialize()) @@ -86,7 +51,9 @@ def get_public_keys(self, issuer: str) -> list[dict]: trust_source = self._get_trust_source(issuer) if not trust_source.keys: - raise Exception(f"no trust evaluator can provide cyptographic material for {issuer}: searched among: {self.extractors_names}") + raise Exception( + f"no trust evaluator can provide cyptographic material for {issuer}: searched among: {self.handlers_names}" + ) return trust_source.public_keys @@ -98,7 +65,7 @@ def get_metadata(self, issuer: str) -> dict: trust_source = self._get_trust_source(issuer) if not trust_source.metadata: - raise Exception(f"no trust evaluator can provide metadata for {issuer}: searched among: {self.extractors_names}") + raise Exception(f"no trust evaluator can provide metadata for {issuer}: searched among: {self.handlers_names}") return trust_source.metadata @@ -114,7 +81,7 @@ def get_policies(self, issuer: str) -> dict: trust_source = self._get_trust_source(issuer) if not trust_source.policies: - raise Exception(f"no trust evaluator can provide policies for {issuer}: searched among: {self.extractors_names}") + raise Exception(f"no trust evaluator can provide policies for {issuer}: searched among: {self.handlers_names}") return trust_source.policies @@ -122,7 +89,32 @@ def get_selfissued_jwt_header_trust_parameters(self, issuer: str) -> dict: trust_source = self._get_trust_source(issuer) if not trust_source.trust_params: - raise Exception(f"no trust evaluator can provide trust parameters for {issuer}: searched among: {self.extractors_names}") + raise Exception(f"no trust evaluator can provide trust parameters for {issuer}: searched among: {self.handlers_names}") return trust_source.trust_params + + @staticmethod + def from_config(config: dict, db_engine: DBEngine) -> 'CombinedTrustEvaluator': + handlers = [] + + for handler_name, handler_config in config.items(): + try: + trust_handler = dynamic_class_loader( + handler_config["module"], + handler_config["class"], + handler_config["config"] + ) + except Exception as e: + raise TrustConfigurationError(f"invalid configuration for {handler_name}: {e}", e) + + if not isinstance(trust_handler, TrustHandlerInterface): + raise TrustConfigurationError(f"class {trust_handler.__class__} does not satisfy the interface TrustEvaluator") + + handlers.append(trust_handler) + + if not handlers: + logger.warning("No configured trust model, using direct trust model") + handlers.append(DirectTrustJWTHandler()) + + return CombinedTrustEvaluator(handlers, db_engine) From c28fa759e69f169242f4015812bcbe2becb4f478 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:31:04 +0200 Subject: [PATCH 35/44] feat: updated inteface --- pyeudiw/trust/handler/interface.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/pyeudiw/trust/handler/interface.py b/pyeudiw/trust/handler/interface.py index bb7db351..afa25bbd 100644 --- a/pyeudiw/trust/handler/interface.py +++ b/pyeudiw/trust/handler/interface.py @@ -1,20 +1,15 @@ from pyeudiw.trust.model.trust_source import TrustSourceData class TrustHandlerInterface: - @staticmethod - def extract( - self, - issuer: str, - trust_source: TrustSourceData, - data_endpoint: str, - httpc_params: dict - ) -> TrustSourceData: + def extract(self, issuer: str, trust_source: TrustSourceData) -> TrustSourceData: NotImplementedError - @staticmethod + def get_metadata(self, issuer: str, trust_source: TrustSourceData) -> TrustSourceData: + NotImplementedError + def verify() -> bool: NotImplementedError - @staticmethod - def name() -> str: - NotImplementedError \ No newline at end of file + @property + def name(self) -> str: + return self.__class__.__name__ \ No newline at end of file From 014732afa24df1cf67bd6ad57db6141b0a11c652 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:31:39 +0200 Subject: [PATCH 36/44] feat: added dummy Federation handler --- pyeudiw/trust/handler/federation.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 pyeudiw/trust/handler/federation.py diff --git a/pyeudiw/trust/handler/federation.py b/pyeudiw/trust/handler/federation.py new file mode 100644 index 00000000..cfbf32ac --- /dev/null +++ b/pyeudiw/trust/handler/federation.py @@ -0,0 +1,15 @@ +from pyeudiw.trust.handler.interface import TrustHandlerInterface +from pyeudiw.tools.base_logger import BaseLogger + +class FederationHandler(TrustHandlerInterface, BaseLogger): + def __init__(self, **kargs): + pass + + def extract(self, issuer, trust_source): + pass + + def get_metadata(self, issuer, trust_source): + pass + + def verify(): + pass \ No newline at end of file From b135479ef4ae3bc71b02dec12f971051334bef8f Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:32:32 +0200 Subject: [PATCH 37/44] fix: CombinedTrustEvaluator instantiation --- pyeudiw/satosa/default/openid4vp_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyeudiw/satosa/default/openid4vp_backend.py b/pyeudiw/satosa/default/openid4vp_backend.py index 4db4edbd..58618a43 100644 --- a/pyeudiw/satosa/default/openid4vp_backend.py +++ b/pyeudiw/satosa/default/openid4vp_backend.py @@ -17,7 +17,7 @@ from pyeudiw.storage.exceptions import StorageWriteError from pyeudiw.tools.mobile import is_smartphone from pyeudiw.tools.utils import iat_now -from pyeudiw.trust.dynamic import CombinedTrustEvaluator, dynamic_trust_evaluators_loader +from pyeudiw.trust.dynamic import CombinedTrustEvaluator from ..interfaces.openid4vp_backend import OpenID4VPBackendInterface @@ -95,7 +95,7 @@ def __init__( self.response_code_helper = ResponseCodeSource(self.config["response_code"]["sym_key"]) trust_configuration = self.config.get("trust", {}) - self.trust_evaluator = CombinedTrustEvaluator(dynamic_trust_evaluators_loader(trust_configuration), self.db_engine) + self.trust_evaluator = CombinedTrustEvaluator.from_config(trust_configuration, self.db_engine) self.init_trust_resources() # Questo carica risorse, metadata endpoint (sotto formate di attributi con pattern *_endpoint) etc, che satosa deve pubblicare def register_endpoints(self) -> list[tuple[str, Callable[[Context], Response]]]: From 33ce2ba21a08b47bb1881b76575ac0cf2d11ee49 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:44:08 +0200 Subject: [PATCH 38/44] tests: added tests for trust module --- pyeudiw/tests/trust/__init__.py | 30 +++++ pyeudiw/tests/trust/mock_trust_handler.py | 33 +++++ pyeudiw/tests/trust/test_dynamic.py | 140 ++++++---------------- 3 files changed, 102 insertions(+), 101 deletions(-) create mode 100644 pyeudiw/tests/trust/__init__.py create mode 100644 pyeudiw/tests/trust/mock_trust_handler.py diff --git a/pyeudiw/tests/trust/__init__.py b/pyeudiw/tests/trust/__init__.py new file mode 100644 index 00000000..13dfa46d --- /dev/null +++ b/pyeudiw/tests/trust/__init__.py @@ -0,0 +1,30 @@ +correct_config = { + "mock": { + "module": "pyeudiw.tests.trust.mock_trust_handler", + "class": "MockTrustHandler", + "config": {} + }, + "direct_trust_sd_jwt_vc": { + "module": "pyeudiw.trust.handler.direct_trust_sd_jwt_vc", + "class": "DirectTrustJWTHandler", + "config": { + "jwk_endpoint": "/.well-known/jwt-vc-issuer", + "httpc_params": { + "connection": { + "ssl": True + }, + "session": { + "timeout": 6 + } + } + } + }, +} + +not_conformant = { + "not_conformant": { + "module": "pyeudiw.tests.trust.mock_trust_handler", + "class": "MockTrustEvaluator", + "config": {} + } +} \ No newline at end of file diff --git a/pyeudiw/tests/trust/mock_trust_handler.py b/pyeudiw/tests/trust/mock_trust_handler.py new file mode 100644 index 00000000..08fb3731 --- /dev/null +++ b/pyeudiw/tests/trust/mock_trust_handler.py @@ -0,0 +1,33 @@ +from pyeudiw.trust.handler.interface import TrustHandlerInterface +from pyeudiw.trust.model.trust_source import TrustSourceData + +mock_jwk = { + "crv": "P-256", + "kid": "qTo9RGpuU_CSolt6GZmndLyPXJJa48up5dH1YbxVDPs", + "kty": "EC", + "use": "sig", + "x": "xu0FC3OQLgsea27rL0-d2CpVyKijjwl8tF6HB-3zLUg", + "y": "fUEsB8IrX2DgzqABfVsCody1RypAXX54fXQ1keoPP5Y" +} + +class MockTrustHandler(TrustHandlerInterface): + """ + Mock realization of TrustEvaluator for testing purposes only + """ + def get_metadata(self, issuer: str, trust_source: TrustSourceData) -> dict: + trust_source.metadata = { + "json_key": "json_value" + } + return trust_source + + def extract(self, issuer: str, trust_source: TrustSourceData) -> TrustSourceData: + trust_source = self.get_metadata(issuer, trust_source) + trust_source.keys.append(mock_jwk) + return trust_source + +class NonConformatTrustHandler: + def get_metadata(self, issuer: str, trust_source: TrustSourceData) -> dict: + return trust_source + + def extract(self, issuer: str, trust_source: TrustSourceData) -> TrustSourceData: + return trust_source \ No newline at end of file diff --git a/pyeudiw/tests/trust/test_dynamic.py b/pyeudiw/tests/trust/test_dynamic.py index 628d9756..2cc64164 100644 --- a/pyeudiw/tests/trust/test_dynamic.py +++ b/pyeudiw/tests/trust/test_dynamic.py @@ -1,112 +1,50 @@ -from pyeudiw.trust.default import DEFAULT_DIRECT_TRUST_SD_JWC_VC_PARAMS -from pyeudiw.trust.default.direct_trust_sd_jwt_vc import DirectTrustSdJwtVc -from pyeudiw.trust.dynamic import CombinedTrustEvaluator, dynamic_trust_evaluators_loader -from pyeudiw.trust.interface import TrustEvaluator +from uuid import uuid4 +from pyeudiw.trust.dynamic import CombinedTrustEvaluator +from pyeudiw.tests.trust import correct_config, not_conformant +from pyeudiw.tests.settings import CONFIG +from pyeudiw.storage.db_engine import DBEngine +from pyeudiw.tests.trust.mock_trust_handler import MockTrustHandler +from pyeudiw.trust.handler.direct_trust_sd_jwt_vc import DirectTrustJWTHandler +from pyeudiw.trust.exceptions import TrustConfigurationError +def test_trust_CombinedTrusstEvaluation_handler_loading(): + trust_ev = CombinedTrustEvaluator.from_config(correct_config, DBEngine(CONFIG["storage"])) -class MockTrustEvaluator(TrustEvaluator): - """Mock realization of TrustEvaluator for testing purposes only - """ - mock_jwk = { - "crv": "P-256", - "kid": "qTo9RGpuU_CSolt6GZmndLyPXJJa48up5dH1YbxVDPs", - "kty": "EC", - "use": "sig", - "x": "xu0FC3OQLgsea27rL0-d2CpVyKijjwl8tF6HB-3zLUg", - "y": "fUEsB8IrX2DgzqABfVsCody1RypAXX54fXQ1keoPP5Y" - } + assert trust_ev + assert len(trust_ev.handlers) == 2 + assert isinstance(trust_ev.handlers[0], MockTrustHandler) + assert isinstance(trust_ev.handlers[1], DirectTrustJWTHandler) - def __init__(self): - pass - def get_public_keys(self, issuer: str) -> list[dict]: - return [ - MockTrustEvaluator.mock_jwk - ] +def test_not_conformant_CombinedTrusstEvaluation_handler_loading(): + try: + CombinedTrustEvaluator.from_config(not_conformant, DBEngine(CONFIG["storage"])) + assert False + except TrustConfigurationError: + assert True - def get_metadata(self, issuer: str) -> dict: - return { - "json_key": "json_value" - } +def test_if_no_conf_default_handler_instanciated(): + trust_ev = CombinedTrustEvaluator.from_config({}, DBEngine(CONFIG["storage"])) - def is_revoked(self, issuer: str) -> bool: - return False + assert len(trust_ev.handlers) == 1 + assert isinstance(trust_ev.handlers[0], DirectTrustJWTHandler) - def get_policies(self, issuer: str) -> dict: - return {} +def test_public_key_and_metadata_retrive(): + db_engine = DBEngine(CONFIG["storage"]) + trust_ev = CombinedTrustEvaluator.from_config(correct_config, db_engine) -def test_trust_evaluators_loader(): - config = { - "mock": { - "module": "pyeudiw.tests.trust.test_dynamic", - "class": "MockTrustEvaluator", - "config": {} - }, - "direct_trust_sd_jwt_vc": { - "module": "pyeudiw.trust.default.direct_trust_sd_jwt_vc", - "class": "DirectTrustSdJwtVc", - "config": { - "jwk_endpoint": "/.well-known/jwt-vc-issuer", - "httpc_params": { - "connection": { - "ssl": True - }, - "session": { - "timeout": 6 - } - } - } - }, - "federation": { - "module": "pyeudiw.trust.default.federation", - "class": "FederationTrustModel", - "config": { - "metadata_type": "wallet_relying_party", - "authority_hints": [ - "http://127.0.0.1:8000" - ], - "trust_anchors": [ - { - "public_keys": [] - }, - "http://127.0.0.1:8000" - ], - "default_sig_alg": "RS256", - "trust_marks": [], - "federation_entity_metadata": { - "organization_name": "Developers Italia SATOSA OpenID4VP backend", - "homepage_uri": "https://developers.italia.it", - "policy_uri": "https://developers.italia.it", - "tos_uri": "https://developers.italia.it", - "logo_uri": "https://developers.italia.it/assets/icons/logo-it.svg" - }, - "federation_jwks": [ - { - "kty": "RSA", - "d": "QUZsh1NqvpueootsdSjFQz-BUvxwd3Qnzm5qNb-WeOsvt3rWMEv0Q8CZrla2tndHTJhwioo1U4NuQey7znijhZ177bUwPPxSW1r68dEnL2U74nKwwoYeeMdEXnUfZSPxzs7nY6b7vtyCoA-AjiVYFOlgKNAItspv1HxeyGCLhLYhKvS_YoTdAeLuegETU5D6K1xGQIuw0nS13Icjz79Y8jC10TX4FdZwdX-NmuIEDP5-s95V9DMENtVqJAVE3L-wO-NdDilyjyOmAbntgsCzYVGH9U3W_djh4t3qVFCv3r0S-DA2FD3THvlrFi655L0QHR3gu_Fbj3b9Ybtajpue_Q", - "e": "AQAB", - "kid": "9Cquk0X-fNPSdePQIgQcQZtD6J0IjIRrFigW2PPK_-w", - "n": "utqtxbs-jnK0cPsV7aRkkZKA9t4S-WSZa3nCZtYIKDpgLnR_qcpeF0diJZvKOqXmj2cXaKFUE-8uHKAHo7BL7T-Rj2x3vGESh7SG1pE0thDGlXj4yNsg0qNvCXtk703L2H3i1UXwx6nq1uFxD2EcOE4a6qDYBI16Zl71TUZktJwmOejoHl16CPWqDLGo9GUSk_MmHOV20m4wXWkB4qbvpWVY8H6b2a0rB1B1YPOs5ZLYarSYZgjDEg6DMtZ4NgiwZ-4N1aaLwyO-GLwt9Vf-NBKwoxeRyD3zWE2FXRFBbhKGksMrCGnFDsNl5JTlPjaM3kYyImE941ggcuc495m-Fw", - "p": "2zmGXIMCEHPphw778YjVTar1eycih6fFSJ4I4bl1iq167GqO0PjlOx6CZ1-OdBTVU7HfrYRiUK_BnGRdPDn-DQghwwkB79ZdHWL14wXnpB5y-boHz_LxvjsEqXtuQYcIkidOGaMG68XNT1nM4F9a8UKFr5hHYT5_UIQSwsxlRQ0", - "q": "2jMFt2iFrdaYabdXuB4QMboVjPvbLA-IVb6_0hSG_-EueGBvgcBxdFGIZaG6kqHqlB7qMsSzdptU0vn6IgmCZnX-Hlt6c5X7JB_q91PZMLTO01pbZ2Bk58GloalCHnw_mjPh0YPviH5jGoWM5RHyl_HDDMI-UeLkzP7ImxGizrM" - } - ] - } - } - } - - trust_sources = dynamic_trust_evaluators_loader(config) - assert "mock" in trust_sources - assert trust_sources["mock"].__class__.__name__ == "MockTrustEvaluator" - assert "direct_trust_sd_jwt_vc" in trust_sources - assert trust_sources["direct_trust_sd_jwt_vc"].__class__.__name__ == "DirectTrustSdJwtVc" + uuid_url = f"http://{str(uuid4())}.issuer.it" + pub_keys = trust_ev.get_public_keys(uuid_url) + trust_source = db_engine.get_trust_source(uuid_url) -def test_combined_trust_evaluator(): - evaluators = { - "mock": MockTrustEvaluator(), - "direct_trust_sd_jwt_vc": DirectTrustSdJwtVc(**DEFAULT_DIRECT_TRUST_SD_JWC_VC_PARAMS) - } - combined = CombinedTrustEvaluator(evaluators) - assert MockTrustEvaluator.mock_jwk in combined.get_public_keys("mock_issuer") + assert trust_source + assert trust_source["keys"][0]["kid"] == "qTo9RGpuU_CSolt6GZmndLyPXJJa48up5dH1YbxVDPs" + assert trust_source["metadata"] == {"json_key": "json_value"} + + assert pub_keys[0]["kid"] == "qTo9RGpuU_CSolt6GZmndLyPXJJa48up5dH1YbxVDPs" + + metadata = trust_ev.get_metadata(uuid_url) + + assert metadata == {"json_key": "json_value"} \ No newline at end of file From 06e42d0ae8f18bc0175fc8a5dbc23cb13bfad8c1 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 25 Oct 2024 18:44:23 +0200 Subject: [PATCH 39/44] fix: parameter handling --- pyeudiw/trust/model/trust_source.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyeudiw/trust/model/trust_source.py b/pyeudiw/trust/model/trust_source.py index 99649d84..79f612be 100644 --- a/pyeudiw/trust/model/trust_source.py +++ b/pyeudiw/trust/model/trust_source.py @@ -37,7 +37,8 @@ def __init__( metadata: dict = {}, revoked: bool = False, keys: list[dict] = [], - trust_params: dict[str, dict[str, any]] = {} + trust_params: dict[str, dict[str, any]] = {}, + **kwargs ) -> None: self.entity_id = entity_id self.policies = policies @@ -45,6 +46,8 @@ def __init__( self.revoked = revoked self.keys = keys + self.additional_data = kwargs + self.trust_params = [TrustParameterData(**tp) for tp in trust_params] def add_key(self, key: dict) -> None: From a71e45e8d2417ce89da9ef81455d14694c652a66 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 31 Oct 2024 18:36:27 +0100 Subject: [PATCH 40/44] chore: removed unused files --- pyeudiw/jwk/schema.py | 66 -------------- pyeudiw/tests/trust/default/settings.py | 24 ----- .../tests/trust/default/test_direct_trust.py | 20 ----- .../trust/default/direct_trust_sd_jwt_vc.py | 89 ------------------- pyeudiw/trust/default/x509.py | 6 -- 5 files changed, 205 deletions(-) delete mode 100644 pyeudiw/jwk/schema.py delete mode 100644 pyeudiw/tests/trust/default/settings.py delete mode 100644 pyeudiw/tests/trust/default/test_direct_trust.py delete mode 100644 pyeudiw/trust/default/direct_trust_sd_jwt_vc.py delete mode 100644 pyeudiw/trust/default/x509.py diff --git a/pyeudiw/jwk/schema.py b/pyeudiw/jwk/schema.py deleted file mode 100644 index 5bee3e05..00000000 --- a/pyeudiw/jwk/schema.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import List, Literal, Optional - -from pydantic import BaseModel, field_validator - - -class JwkSchema(BaseModel): - kid: str # Base64url-encoded thumbprint string - kty: Literal["EC", "RSA"] - alg: Optional[ - Literal[ - "RS256", - "RS384", - "RS512", - "ES256", - "ES384", - "ES512", - "PS256", - "PS384", - "PS512", - ] - ] = None - use: Optional[Literal["sig", "enc"]] = None - n: Optional[str] = None # Base64urlUInt-encoded - e: Optional[str] = None # Base64urlUInt-encoded - - def check_value_for_rsa(value, name, values): - if "EC" == values.get("kty") and value: - raise ValueError(f"{name} must be present only for kty = RSA") - - def check_value_for_ec(value, name, values): - if "RSA" == values.get("kty") and value: - raise ValueError(f"{name} must be present only for kty = EC") - - @field_validator("n") - def validate_n(cls, n_value, values): - cls.check_value_for_rsa(n_value, "n", values.data) - - @field_validator("e") - def validate_e(cls, e_value, values): - cls.check_value_for_rsa(e_value, "e", values.data) - - -class JwkSchemaEC(JwkSchema): - x: Optional[str] # Base64url-encoded - y: Optional[str] # Base64url-encoded - crv: Optional[Literal["P-256", "P-384", "P-521"]] - - @field_validator("x") - def validate_x(cls, x_value, values): - cls.check_value_for_ec(x_value, "x", values.data) - - @field_validator("y") - def validate_y(cls, y_value, values): - cls.check_value_for_ec(y_value, "y", values.data) - - @field_validator("crv") - def validate_crv(cls, crv_value, values): - cls.check_value_for_ec(crv_value, "crv", values.data) - - -class JwksSchemaEC(BaseModel): - keys: List[JwkSchemaEC] - - -class JwksSchema(BaseModel): - keys: List[JwkSchema] diff --git a/pyeudiw/tests/trust/default/settings.py b/pyeudiw/tests/trust/default/settings.py deleted file mode 100644 index 1d325955..00000000 --- a/pyeudiw/tests/trust/default/settings.py +++ /dev/null @@ -1,24 +0,0 @@ -import json -import requests - - -issuer = "https://credential-issuer.example/vct/" -issuer_jwk = { - "kty": "EC", - "kid": "MGaAh57cQghnevfWusalp0lNFXTzz2kHnkzO9wOjHq4", - "crv": "P-256", - "x": "S57KP4yGauTJJuNvO-wgWr2h_BYsatYUA1xW8Nae8i4", - "y": "66DmArglfyJODHAzZsIiPTY24gK70eeXPbpT4Nk0768" -} -issuer_vct_md = { - "issuer": issuer, - "jwks": { - "keys": [ - issuer_jwk - ] - } -} -jwt_vc_issuer_endpoint_response = requests.Response() -jwt_vc_issuer_endpoint_response.status_code = 200 -jwt_vc_issuer_endpoint_response.headers.update({"Content-Type": "application/json"}) -jwt_vc_issuer_endpoint_response._content = json.dumps(issuer_vct_md).encode('utf-8') diff --git a/pyeudiw/tests/trust/default/test_direct_trust.py b/pyeudiw/tests/trust/default/test_direct_trust.py deleted file mode 100644 index c062f72d..00000000 --- a/pyeudiw/tests/trust/default/test_direct_trust.py +++ /dev/null @@ -1,20 +0,0 @@ -import unittest.mock - -from pyeudiw.trust.default import DEFAULT_DIRECT_TRUST_SD_JWC_VC_PARAMS -from pyeudiw.trust.default.direct_trust_sd_jwt_vc import DirectTrustSdJwtVc - -from pyeudiw.tests.trust.default.settings import issuer, jwt_vc_issuer_endpoint_response -from pyeudiw.tests.trust.default.settings import issuer_jwk as expected_jwk - - -def test_direct_trust_jwk(): - mocked_issuer_jwt_vc_issuer_endpoint = unittest.mock.patch("pyeudiw.vci.jwks_provider.get_http_url", return_value=[jwt_vc_issuer_endpoint_response]) - mocked_issuer_jwt_vc_issuer_endpoint.start() - - trust_source = DirectTrustSdJwtVc(**DEFAULT_DIRECT_TRUST_SD_JWC_VC_PARAMS) - obtained_jwks = trust_source.get_public_keys(issuer) - - mocked_issuer_jwt_vc_issuer_endpoint.stop() - - assert len(obtained_jwks) == 1, f"expected 1 jwk, obtained {len(obtained_jwks)}" - assert expected_jwk == obtained_jwks[0] diff --git a/pyeudiw/trust/default/direct_trust_sd_jwt_vc.py b/pyeudiw/trust/default/direct_trust_sd_jwt_vc.py deleted file mode 100644 index f45cd775..00000000 --- a/pyeudiw/trust/default/direct_trust_sd_jwt_vc.py +++ /dev/null @@ -1,89 +0,0 @@ -import os -import time -from typing import Optional - -from pyeudiw.tools.utils import get_http_url -from pyeudiw.trust.interface import TrustEvaluator -from pyeudiw.vci.jwks_provider import CachedVciJwksSource, RemoteVciJwksSource, VciJwksSource -from pyeudiw.vci.utils import cacheable_get_http_url - - -DEFAULT_ISSUER_JWK_ENDPOINT = "/.well-known/jwt-vc-issuer" -DEFAULT_METADATA_ENDPOINT = "/.well-known/openid-credential-issuer" -DEFAULT_DIRECT_TRUST_SD_JWC_VC_PARAMS = { - "httpc_params": { - "connection": { - "ssl": os.getenv("PYEUDIW_HTTPC_SSL", True) - }, - "session": { - "timeout": os.getenv("PYEUDIW_HTTPC_TIMEOUT", 6) - } - } -} - - -class DirectTrust(TrustEvaluator): - pass - - -class DirectTrustSdJwtVc(DirectTrust): - """ - DirectTrust trust models assumes that an issuer is always trusted, in the sense - that no trust verification actually happens. The issuer is assumed to be an URI - and its keys and metadata information are publicly exposed on the web. - Such keys/metadata can always be fetched remotely and long as the issuer is - available. - """ - def __init__(self, httpc_params: Optional[dict] = None, cache_ttl: int = 0, jwk_endpoint: str = DEFAULT_ISSUER_JWK_ENDPOINT, - metadata_endpoint: str = DEFAULT_METADATA_ENDPOINT): - if httpc_params is None: - self.httpc_params = DEFAULT_DIRECT_TRUST_SD_JWC_VC_PARAMS["httpc_params"] - self.httpc_params = httpc_params - self.cache_ttl = cache_ttl - self.jwk_endpoint = jwk_endpoint - self.metadata_endpoint = metadata_endpoint - self._vci_jwks_source: VciJwksSource = None - - # TODO: remove the if statement below and integrate in an unique class that uses the cache and non-cache approach - if self.cache_ttl == 0: - self._vci_jwks_source = RemoteVciJwksSource(httpc_params, jwk_endpoint) - else: - self._vci_jwks_source = CachedVciJwksSource(self.cache_ttl, httpc_params, jwk_endpoint) - - def get_public_keys(self, issuer: str) -> list[dict]: - """ - Fetches the public key of the issuer by querying a given endpoint. - Previous responses might or might not be cached based on the cache_ttl - parameter. - - :returns: a list of jwk(s) - """ - return self._vci_jwks_source.get_jwks(issuer) - - def get_metadata(self, issuer: str) -> dict: - """ - Fetches the public metadata of an issuer by interrogating a given - endpoint. The endpoint must yield information in a format that - can be transalted to a meaning dictionary (such as json) - - :returns: a dictionary of metadata information - """ - if not issuer: - raise ValueError("invalid issuer: cannot be empty value") - - issuer_normalized = [issuer if issuer[-1] != '/' else issuer[:-1]] - url = issuer_normalized + self.metadata_endpoint - if self.cache_ttl == 0: - return get_http_url(url, self.httpc_params)[0].json() - ttl_timestamp = round(time.time() / self.cache_ttl) - return cacheable_get_http_url(ttl_timestamp, url, self.httpc_params)[0].json() - - def __str__(self) -> str: - return ( - f"DirectTrustSdJwtVc(" - f"httpc_params={self.httpc_params}, " - f"cache_ttl={self.cache_ttl}, " - f"jwk_endpoint={self.jwk_endpoint}, " - f"metadata_endpoint={self.metadata_endpoint}" - ")" - ) diff --git a/pyeudiw/trust/default/x509.py b/pyeudiw/trust/default/x509.py deleted file mode 100644 index 39e0adca..00000000 --- a/pyeudiw/trust/default/x509.py +++ /dev/null @@ -1,6 +0,0 @@ -from pyeudiw.trust.interface import TrustEvaluator - - -class X509TrustModel(TrustEvaluator): - def __init__(self, **kwargs): - pass From 97047da7c3b3a8b8137289b838bf2e36d14137fa Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 31 Oct 2024 18:37:28 +0100 Subject: [PATCH 41/44] feat: db code refactoring --- pyeudiw/storage/db_engine.py | 6 +- pyeudiw/storage/mongo_storage.py | 115 ++++++++++++++++--------------- 2 files changed, 61 insertions(+), 60 deletions(-) diff --git a/pyeudiw/storage/db_engine.py b/pyeudiw/storage/db_engine.py index 6141291a..7e3620c6 100644 --- a/pyeudiw/storage/db_engine.py +++ b/pyeudiw/storage/db_engine.py @@ -88,7 +88,7 @@ def write(self, method: str, *args, **kwargs): except Exception as e: self._log_critical( e.__class__.__name__, - f"Error {_err_msg} on {db_name} {storage}: {str(e)}" + f"Error {_err_msg} on {db_name}: {str(e)}" ) if not replica_count: @@ -165,8 +165,8 @@ def add_trust_attestation(self, entity_id: str, attestation: list[str] = [], exp def add_trust_attestation_metadata(self, entity_id: str, metadat_type: str, metadata: dict) -> str: return self.write("add_trust_attestation_metadata", entity_id, metadat_type, metadata) - def add_trust_source(self, entity_id: str, trust_source: dict) -> str: - return self.write("add_trust_source", entity_id, trust_source) + def add_trust_source(self, trust_source: dict) -> str: + return self.write("add_trust_source", trust_source) def get_trust_source(self, entity_id: str) -> dict: return self.get("get_trust_source", entity_id) diff --git a/pyeudiw/storage/mongo_storage.py b/pyeudiw/storage/mongo_storage.py index 0b93481e..7c4bbbc2 100644 --- a/pyeudiw/storage/mongo_storage.py +++ b/pyeudiw/storage/mongo_storage.py @@ -255,28 +255,26 @@ def has_trust_source(self, entity_id: str) -> bool: entity_id ) - def _add_entry( + def _upsert_entry( self, + key_label: str, collection: str, - entity_id: str, - attestation: Union[str, dict], - exp: datetime - ) -> str: - - meth_suffix = collection[:-1] - if getattr(self, f"has_{meth_suffix}")(entity_id): - # TODO: bug detected. Commentato l'update e lasciato il raise dell'eccezione - # l'attestation passata come parametro non è quello che si aspetta il metodo di update - # bensì è l'intero oggetto trust_attestation - - # # update it - # getattr(self, f"update_{meth_suffix}")(entity_id, attestation, exp) - # return entity_id - raise ChainAlreadyExist(f"Chain with entity id {entity_id} already exists") - + data: Union[str, dict] + ) -> tuple[str, dict]: db_collection = getattr(self, collection) - db_collection.insert_one(attestation) - return entity_id + + document_status = db_collection.update_one( + {key_label: data[key_label]}, + {"$set": data}, + upsert=True + ) + + if not document_status.acknowledged: + raise StorageEntryUpdateFailed( + "Trust Anchor matched count is ZERO" + ) + + return document_status def _update_attestation_metadata(self, entity: dict, attestation: list[str], exp: datetime, trust_type: TrustType, jwks: list[dict]): trust_name = trust_type_map[trust_type] @@ -295,7 +293,10 @@ def _update_attestation_metadata(self, entity: dict, attestation: list[str], exp return entity - def _update_anchor_metadata(self, entity: dict, attestation: list[str], exp: datetime, trust_type: TrustType): + def _update_anchor_metadata(self, entity: dict, attestation: list[str], exp: datetime, trust_type: TrustType, entity_id: str): + if entity.get("entity_id", None) is None: + entity["entity_id"] = entity_id + trust_name = trust_type_map[trust_type] trust_field = trust_anchor_field_map.get(trust_type, None) @@ -307,6 +308,7 @@ def _update_anchor_metadata(self, entity: dict, attestation: list[str], exp: dat entity[trust_name] = trust_entity + return entity def add_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType, jwks: list[dict]) -> str: @@ -320,14 +322,16 @@ def add_trust_attestation(self, entity_id: str, attestation: list[str], exp: dat updated_entity = self._update_attestation_metadata( entity, attestation, exp, trust_type, jwks) - - return self._add_entry( - self.storage_conf["db_trust_attestations_collection"], entity_id, updated_entity, exp + + self._upsert_entry( + "entity_id", self.storage_conf["db_trust_attestations_collection"], updated_entity ) + + return entity_id - def add_trust_source(self, entity_id: str, trust_source: dict) -> str: - return self._add_entry( - "trust_sources", entity_id, trust_source, trust_source.get("exp") + def add_trust_source(self, trust_source: dict) -> str: + return self._upsert_entry( + "entity_id", "trust_sources", trust_source ) def add_trust_attestation_metadata(self, entity_id: str, metadata_type: str, metadata: dict): @@ -340,56 +344,53 @@ def add_trust_attestation_metadata(self, entity_id: str, metadata_type: str, met entity["metadata"][metadata_type] = metadata - return self._update_trust_attestation(self.storage_conf["db_trust_attestations_collection"], entity_id, entity) - - def add_trust_anchor(self, entity_id: str, entity_configuration: str, exp: datetime, trust_type: TrustType): - if self.has_trust_anchor(entity_id): - return self.update_trust_anchor(entity_id, entity_configuration, exp, trust_type) - else: - entity = { - "entity_id": entity_id, - "federation": {}, - "x509": {} - } - - updated_entity = self._update_anchor_metadata( - entity, entity_configuration, exp, trust_type) - return self._add_entry(self.storage_conf["db_trust_anchors_collection"], entity_id, updated_entity, exp) - - def _update_trust_attestation(self, collection: str, entity_id: str, entity: dict) -> str: - if not self._has_db_entity(collection, entity_id): + if not self._has_db_entity( + self.storage_conf["db_trust_attestations_collection"], entity_id + ): raise ChainNotExist(f"Chain with entity id {entity_id} not exist") - documentStatus = self.trust_attestations.update_one( - {"entity_id": entity_id}, - {"$set": entity} + documentStatus = self._upsert_entry( + "entity_id", self.storage_conf["db_trust_attestations_collection"], entity ) + return documentStatus + def add_trust_anchor(self, entity_id: str, entity_configuration: str, exp: datetime, trust_type: TrustType): + entity = { + "entity_id": entity_id, + "federation": {}, + "x509": {} + } + + updated_entity = self._update_anchor_metadata( + entity, entity_configuration, exp, trust_type, entity_id) + + self._upsert_entry("entity_id", self.storage_conf["db_trust_anchors_collection"], updated_entity) + + return entity_id + + def update_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType, jwks: list[dict]) -> str: old_entity = self._get_db_entity( self.storage_conf["db_trust_attestations_collection"], entity_id) or {} upd_entity = self._update_attestation_metadata( old_entity, attestation, exp, trust_type, jwks) - return self._update_trust_attestation(self.storage_conf["db_trust_attestations_collection"], entity_id, upd_entity) + return self._upsert_entry( + "entity_id", self.storage_conf["db_trust_attestations_collection"], upd_entity + ) def update_trust_anchor(self, entity_id: str, entity_configuration: str, exp: datetime, trust_type: TrustType) -> str: old_entity = self._get_db_entity( self.storage_conf["db_trust_attestations_collection"], entity_id) or {} upd_entity = self._update_anchor_metadata( - old_entity, entity_configuration, exp, trust_type) + old_entity, entity_configuration, exp, trust_type, entity_id) if not self.has_trust_anchor(entity_id): raise ChainNotExist(f"Chain with entity id {entity_id} not exist") - - documentStatus = self.trust_anchors.update_one( - {"entity_id": entity_id}, - {"$set": upd_entity} + + documentStatus = self._upsert_entry( + "entity_id", self.storage_conf["db_trust_anchors_collection"], upd_entity ) - if not documentStatus.matched_count: - raise StorageEntryUpdateFailed( - "Trust Anchor matched count is ZERO" - ) return documentStatus From cbbadaa3e33ac7e6e2aebb304452010008e7ab25 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 31 Oct 2024 18:37:57 +0100 Subject: [PATCH 42/44] feat: migrated tests --- pyeudiw/tests/trust/handler/__init__.py | 24 +++++++++++++++++++ .../tests/trust/handler/test_direct_trust.py | 22 +++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 pyeudiw/tests/trust/handler/__init__.py create mode 100644 pyeudiw/tests/trust/handler/test_direct_trust.py diff --git a/pyeudiw/tests/trust/handler/__init__.py b/pyeudiw/tests/trust/handler/__init__.py new file mode 100644 index 00000000..1d325955 --- /dev/null +++ b/pyeudiw/tests/trust/handler/__init__.py @@ -0,0 +1,24 @@ +import json +import requests + + +issuer = "https://credential-issuer.example/vct/" +issuer_jwk = { + "kty": "EC", + "kid": "MGaAh57cQghnevfWusalp0lNFXTzz2kHnkzO9wOjHq4", + "crv": "P-256", + "x": "S57KP4yGauTJJuNvO-wgWr2h_BYsatYUA1xW8Nae8i4", + "y": "66DmArglfyJODHAzZsIiPTY24gK70eeXPbpT4Nk0768" +} +issuer_vct_md = { + "issuer": issuer, + "jwks": { + "keys": [ + issuer_jwk + ] + } +} +jwt_vc_issuer_endpoint_response = requests.Response() +jwt_vc_issuer_endpoint_response.status_code = 200 +jwt_vc_issuer_endpoint_response.headers.update({"Content-Type": "application/json"}) +jwt_vc_issuer_endpoint_response._content = json.dumps(issuer_vct_md).encode('utf-8') diff --git a/pyeudiw/tests/trust/handler/test_direct_trust.py b/pyeudiw/tests/trust/handler/test_direct_trust.py new file mode 100644 index 00000000..d7d7cd38 --- /dev/null +++ b/pyeudiw/tests/trust/handler/test_direct_trust.py @@ -0,0 +1,22 @@ +import unittest.mock + +from pyeudiw.trust.handler.direct_trust_sd_jwt_vc import DirectTrustJWTHandler +from pyeudiw.tests.trust.handler import issuer +from pyeudiw.trust.model.trust_source import TrustSourceData +from pyeudiw.tests.trust.handler import issuer_jwk as expected_jwk + + +def test_direct_trust_jwk(): + mocked_issuer_jwt_vc_issuer_endpoint = unittest.mock.patch("pyeudiw.vci.jwks_provider.RemoteVciJwksSource.get_jwks", return_value=[expected_jwk]) + mocked_issuer_jwt_vc_issuer_endpoint.start() + + trust_source = DirectTrustJWTHandler() + + trust_source_data = TrustSourceData(entity_id=issuer) + trust_source.extract(issuer, trust_source_data) + obtained_jwks = trust_source_data.keys + + mocked_issuer_jwt_vc_issuer_endpoint.stop() + + assert len(obtained_jwks) == 1, f"expected 1 jwk, obtained {len(obtained_jwks)}" + assert expected_jwk == obtained_jwks[0] From 9ff52c23de88b97d75915975f6d401da8b8bb223 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 31 Oct 2024 18:38:36 +0100 Subject: [PATCH 43/44] fix: minor fixs --- pyeudiw/trust/dynamic.py | 2 +- pyeudiw/trust/handler/direct_trust_sd_jwt_vc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyeudiw/trust/dynamic.py b/pyeudiw/trust/dynamic.py index 61027c13..fbc2355a 100644 --- a/pyeudiw/trust/dynamic.py +++ b/pyeudiw/trust/dynamic.py @@ -31,7 +31,7 @@ def _extract_trust_source(self, issuer: str) -> Optional[TrustSourceData]: for extractor in self.handlers: trust_source: TrustSourceData = extractor.extract(issuer, trust_source) - self.db_engine.add_trust_source(issuer, trust_source.serialize()) + self.db_engine.add_trust_source(trust_source.serialize()) return trust_source diff --git a/pyeudiw/trust/handler/direct_trust_sd_jwt_vc.py b/pyeudiw/trust/handler/direct_trust_sd_jwt_vc.py index bc7c9e48..16801e44 100644 --- a/pyeudiw/trust/handler/direct_trust_sd_jwt_vc.py +++ b/pyeudiw/trust/handler/direct_trust_sd_jwt_vc.py @@ -43,7 +43,7 @@ def extract(self, issuer: str, trust_source: TrustSourceData) -> TrustSourceData return trust_source def get_metadata(self, issuer: str, trust_source: TrustSourceData) -> TrustSourceData: - issuer_normalized = [issuer if issuer[-1] != '/' else issuer[:-1]] + issuer_normalized = issuer if issuer[-1] != '/' else issuer[:-1] url = issuer_normalized + self.metadata_endpoint try: From 56db57ffb3b688634838f84da8e463c7618acefd Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 31 Oct 2024 18:38:49 +0100 Subject: [PATCH 44/44] chore: removed unused code --- pyeudiw/trust/default/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyeudiw/trust/default/__init__.py b/pyeudiw/trust/default/__init__.py index 4b0e0cea..e69de29b 100644 --- a/pyeudiw/trust/default/__init__.py +++ b/pyeudiw/trust/default/__init__.py @@ -1,6 +0,0 @@ -from pyeudiw.trust.default.direct_trust_sd_jwt_vc import DEFAULT_DIRECT_TRUST_SD_JWC_VC_PARAMS, DirectTrustSdJwtVc -from pyeudiw.trust.interface import TrustEvaluator - - -def default_trust_evaluator() -> TrustEvaluator: - return DirectTrustSdJwtVc(**DEFAULT_DIRECT_TRUST_SD_JWC_VC_PARAMS)