From ea7069d2e8f245f8ec0a0a80a269ac59bfe5bcb2 Mon Sep 17 00:00:00 2001 From: elisanp Date: Fri, 11 Oct 2024 14:43:25 +0200 Subject: [PATCH] wip: storage layer + tests fixes --- example/satosa/pyeudiw_backend.yaml | 9 ++--- pyeudiw/jwt/parse.py | 6 ++-- pyeudiw/openid4vp/interface.py | 7 ++++ pyeudiw/openid4vp/vp_sd_jwt_vc.py | 13 +++++++- pyeudiw/satosa/default/openid4vp_backend.py | 2 +- pyeudiw/satosa/default/response_handler.py | 18 +++++++--- pyeudiw/storage/base_storage.py | 16 ++++++--- pyeudiw/storage/db_engine.py | 14 ++++---- pyeudiw/storage/mongo_storage.py | 37 ++++++++++++--------- pyeudiw/tests/satosa/test_backend.py | 9 ++++- pyeudiw/tests/satosa/test_backend_trust.py | 4 +-- pyeudiw/tests/sd_jwt/test_sdjwt.py | 4 +-- pyeudiw/tests/settings.py | 12 +++---- pyeudiw/tests/trust/test_dynamic.py | 10 +++--- pyeudiw/trust/dynamic.py | 34 +++++++++++++++---- pyeudiw/vci/utils.py | 3 +- 16 files changed, 129 insertions(+), 69 deletions(-) diff --git a/example/satosa/pyeudiw_backend.yaml b/example/satosa/pyeudiw_backend.yaml index 473238bc..c5384282 100644 --- a/example/satosa/pyeudiw_backend.yaml +++ b/example/satosa/pyeudiw_backend.yaml @@ -81,16 +81,11 @@ config: timeout: 6 trust: - direct_trust: - module: pyeudiw.trust.default.direct_trust + 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 diff --git a/pyeudiw/jwt/parse.py b/pyeudiw/jwt/parse.py index c1a8de41..a27ecdd3 100644 --- a/pyeudiw/jwt/parse.py +++ b/pyeudiw/jwt/parse.py @@ -48,10 +48,10 @@ def unsafe_parse_jws(token: str) -> DecodedJwt: def extract_key_identifier(token_header: dict) -> JWK | KeyIdentifier_T: # TODO: the trust evaluation order might be mapped on the same configuration ordering + 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["key"]) + return get_public_key_from_trust_chain(token_header["kid"]) if "x5c" in token_header.keys(): return get_public_key_from_x509_chain(token_header["x5c"]) - if "kid" in token_header.keys(): - return KeyIdentifier_T(token_header["kid"]) raise ValueError(f"unable to infer identifying key from token head: searched among keys {token_header.keys()}") diff --git a/pyeudiw/openid4vp/interface.py b/pyeudiw/openid4vp/interface.py index 3d04bc74..c43beaf4 100644 --- a/pyeudiw/openid4vp/interface.py +++ b/pyeudiw/openid4vp/interface.py @@ -44,5 +44,12 @@ def verify_signature(self, public_key: JWK) -> None: :raises [InvalidSignatureException]: """ raise NotImplementedError + + def verify_challenge(self) -> None: + """ + :raises []: + """ + raise NotImplementedError + # TODO: VP proof of possession verification method should be implemented diff --git a/pyeudiw/openid4vp/vp_sd_jwt_vc.py b/pyeudiw/openid4vp/vp_sd_jwt_vc.py index d5d720f5..6417be08 100644 --- a/pyeudiw/openid4vp/vp_sd_jwt_vc.py +++ b/pyeudiw/openid4vp/vp_sd_jwt_vc.py @@ -3,8 +3,10 @@ from pyeudiw.jwk import JWK from pyeudiw.jwt.parse import KeyIdentifier_T, extract_key_identifier from pyeudiw.jwt.verification import is_jwt_expired +from pyeudiw.openid4vp.exceptions import InvalidVPKeyBinding from pyeudiw.openid4vp.interface import VpTokenParser, VpTokenVerifier -from pyeudiw.sd_jwt.schema import is_sd_jwt_kb_format +from pyeudiw.sd_jwt.exceptions import InvalidKeyBinding, UnsupportedSdAlg +from pyeudiw.sd_jwt.schema import VerifierChallenge, is_sd_jwt_kb_format from pyeudiw.sd_jwt.sd_jwt import SdJwt @@ -39,3 +41,12 @@ def is_expired(self) -> bool: def verify_signature(self, public_key: JWK) -> None: return self.sdjwt.verify_issuer_jwt_signature(public_key) + + def verify_challenge(self) -> None: + challenge : VerifierChallenge = {} + challenge["aud"] = self.verifier_id + challenge["nonce"] = self.verifier_nonce + try: + self.sdjwt.verify_holder_kb_jwt(challenge) + except (UnsupportedSdAlg, InvalidKeyBinding): + raise InvalidVPKeyBinding diff --git a/pyeudiw/satosa/default/openid4vp_backend.py b/pyeudiw/satosa/default/openid4vp_backend.py index 1a3df9f6..4db4edbd 100644 --- a/pyeudiw/satosa/default/openid4vp_backend.py +++ b/pyeudiw/satosa/default/openid4vp_backend.py @@ -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.trust_evaluator = CombinedTrustEvaluator(dynamic_trust_evaluators_loader(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]]]: diff --git a/pyeudiw/satosa/default/response_handler.py b/pyeudiw/satosa/default/response_handler.py index 47a50484..40c13667 100644 --- a/pyeudiw/satosa/default/response_handler.py +++ b/pyeudiw/satosa/default/response_handler.py @@ -12,7 +12,7 @@ from pyeudiw.jwk import JWK from pyeudiw.openid4vp.authorization_response import AuthorizeResponseDirectPost, AuthorizeResponsePayload -from pyeudiw.openid4vp.exceptions import InvalidVPToken, KIDNotFound +from pyeudiw.openid4vp.exceptions import InvalidVPKeyBinding, InvalidVPToken, KIDNotFound from pyeudiw.openid4vp.interface import VpTokenParser, VpTokenVerifier from pyeudiw.openid4vp.vp import Vp from pyeudiw.openid4vp.vp_sd_jwt_vc import VpVcSdJwtParserVerifier @@ -164,11 +164,11 @@ def response_endpoint(self, context: Context, *args: tuple) -> Redirect | JsonRe try: request_session = self._retrieve_session_from_state(authz_payload.state) except AuthorizeUnmatchedResponse as e400: - self._handle_400(context, e400.args[0], e400.args[1]) + return self._handle_400(context, e400.args[0], e400.args[1]) except InvalidInternalStateError as e500: - self._handle_500(context, e500.args[0], "invalid state") + return self._handle_500(context, e500.args[0], "invalid state") except FinalizedSessionError as e400: - self._handle_400(context, e400.args[0], HTTPError(e400.args[0])) + return self._handle_400(context, e400.args[0], HTTPError(e400.args[0])) # the flow below is a simplified algorithm of authentication response processing, where: # (1) we don't check that presentation submission matches definition (yet) @@ -180,13 +180,21 @@ def response_endpoint(self, context: Context, *args: tuple) -> Redirect | JsonRe for vp_token in encoded_vps: # verify vp token and extract user information # TODO: specialized try/except for each call, from line 182 to line 187 - token_parser, token_verifier = self._vp_verifier_factory(authz_payload.presentation_submission, vp_token, request_session) + try: + 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) token_verifier.verify_signature(pub_jwk) + try: + token_verifier.verify_challenge() + except InvalidVPKeyBinding as e: + return self._handle_400(context, f"VP parsing error: {e}") claims = token_parser.get_credentials() iss = token_parser.get_issuer_name() attributes_by_issuer[iss] = claims self._log_debug(context, f"disclosed claims {claims} from issuer {iss}") + all_attributes = self._extract_all_user_attributes(attributes_by_issuer) iss_list_serialized = ";".join(credential_issuers) # marshaling is whatever internal_resp = self._translate_response(all_attributes, iss_list_serialized, context) diff --git a/pyeudiw/storage/base_storage.py b/pyeudiw/storage/base_storage.py index 51ce5b81..067a9e55 100644 --- a/pyeudiw/storage/base_storage.py +++ b/pyeudiw/storage/base_storage.py @@ -7,13 +7,15 @@ class TrustType(Enum): - X509 = 0 - FEDERATION = 1 + X509 = "x509" + FEDERATION = "federation" + DIRECT_TRUST_SD_JWT_VC = "direct_trust_sd_jwt_vc" trust_type_map: dict = { TrustType.X509: "x509", - TrustType.FEDERATION: "federation" + TrustType.FEDERATION: "federation", + TrustType.DIRECT_TRUST_SD_JWT_VC: "direct_trust_sd_jwt_vc" } trust_attestation_field_map: dict = { @@ -170,7 +172,7 @@ def has_trust_anchor(self, entity_id: str) -> bool: """ raise NotImplementedError() - def add_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType) -> str: + def add_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType, jwks: dict) -> str: """ Add a trust attestation. @@ -182,6 +184,8 @@ def add_trust_attestation(self, entity_id: str, attestation: list[str], exp: dat :type exp: datetime :param trust_type: the trust type. :type trust_type: TrustType + :param jwks: cached jwks + :type jwks: dict :returns: the document id. :rtype: str @@ -220,7 +224,7 @@ def add_trust_anchor(self, entity_id: str, entity_configuration: str, exp: datet """ raise NotImplementedError() - def update_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType) -> str: + def update_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType, jwks: dict) -> str: """ Update a trust attestation. @@ -232,6 +236,8 @@ def update_trust_attestation(self, entity_id: str, attestation: list[str], exp: :type exp: datetime :param trust_type: the trust type. :type trust_type: TrustType + :param jwks: cached jwks + :type jwks: dict :returns: the document id. :rtype: str diff --git a/pyeudiw/storage/db_engine.py b/pyeudiw/storage/db_engine.py index e1de39b1..83b7fddb 100644 --- a/pyeudiw/storage/db_engine.py +++ b/pyeudiw/storage/db_engine.py @@ -156,8 +156,8 @@ 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 add_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType = TrustType.FEDERATION) -> str: - return self.write("add_trust_attestation", entity_id, attestation, exp, trust_type) + def add_trust_attestation(self, entity_id: str, attestation: list[str] = [], exp: datetime = None, trust_type: TrustType = TrustType.FEDERATION, jwks: dict = None) -> str: + return self.write("add_trust_attestation", entity_id, attestation, exp, trust_type, jwks) 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) @@ -165,15 +165,15 @@ def add_trust_attestation_metadata(self, entity_id: str, metadat_type: str, meta 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) - def update_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType = TrustType.FEDERATION) -> str: - return self.write("update_trust_attestation", entity_id, attestation, exp, trust_type) + def update_trust_attestation(self, entity_id: str, attestation: list[str] = [], exp: datetime = None, trust_type: TrustType = TrustType.FEDERATION, jwks: dict = None) -> str: + return self.write("update_trust_attestation", entity_id, attestation, exp, trust_type, jwks) - def add_or_update_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType = TrustType.FEDERATION) -> str: + def add_or_update_trust_attestation(self, entity_id: str, attestation: list[str] = [], exp: datetime = None, trust_type: TrustType = TrustType.FEDERATION, jwks: dict = None) -> str: try: self.get_trust_attestation(entity_id) - return self.write("update_trust_attestation", entity_id, attestation, exp, trust_type) + return self.write("update_trust_attestation", entity_id, attestation, exp, trust_type, jwks) except (EntryNotFound, ChainNotExist): - return self.write("add_trust_attestation", entity_id, attestation, exp, trust_type) + return self.write("add_trust_attestation", entity_id, attestation, exp, trust_type, jwks) def update_trust_anchor(self, entity_id: str, entity_configuration: dict, exp: datetime, trust_type: TrustType = TrustType.FEDERATION) -> str: return self.write("update_trust_anchor", entity_id, entity_configuration, exp, trust_type) diff --git a/pyeudiw/storage/mongo_storage.py b/pyeudiw/storage/mongo_storage.py index 443be04b..0badbefa 100644 --- a/pyeudiw/storage/mongo_storage.py +++ b/pyeudiw/storage/mongo_storage.py @@ -12,6 +12,7 @@ trust_anchor_field_map ) from pyeudiw.storage.exceptions import ( + ChainAlreadyExist, ChainNotExist, StorageEntryUpdateFailed ) @@ -238,25 +239,28 @@ def _add_entry( meth_suffix = collection[:-1] if getattr(self, f"has_{meth_suffix}")(entity_id): - # 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 exist" - # ) + # TODO: controllare funzionamento + # 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") db_collection = getattr(self, collection) db_collection.insert_one(attestation) return entity_id - def _update_attestation_metadata(self, entity: dict, attestation: list[str], exp: datetime, trust_type: TrustType): + def _update_attestation_metadata(self, entity: dict, attestation: list[str], exp: datetime, trust_type: TrustType, jwks: dict): trust_name = trust_type_map[trust_type] - trust_field = trust_attestation_field_map[trust_type] + trust_field = trust_attestation_field_map.get(trust_type, None) trust_entity = entity.get(trust_name, {}) - trust_entity[trust_field] = attestation - trust_entity["exp"] = exp + if trust_field and attestation: trust_entity[trust_field] = attestation + if exp: trust_entity["exp"] = exp + if jwks: trust_entity["jwks"] = jwks entity[trust_name] = trust_entity @@ -264,27 +268,28 @@ def _update_attestation_metadata(self, entity: dict, attestation: list[str], exp def _update_anchor_metadata(self, entity: dict, attestation: list[str], exp: datetime, trust_type: TrustType): trust_name = trust_type_map[trust_type] - trust_field = trust_anchor_field_map[trust_type] + trust_field = trust_anchor_field_map.get(trust_type, None) trust_entity = entity.get(trust_name, {}) - trust_entity[trust_field] = attestation + if trust_field and attestation: trust_entity[trust_field] = attestation trust_entity["exp"] = exp entity[trust_name] = trust_entity return entity - def add_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType) -> str: + def add_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType, jwks: dict) -> str: entity = { "entity_id": entity_id, "federation": {}, "x509": {}, + "direct_trust_sd_jwt_vc": {}, "metadata": {} } updated_entity = self._update_attestation_metadata( - entity, attestation, exp, trust_type) + entity, attestation, exp, trust_type, jwks) return self._add_entry( "trust_attestations", entity_id, updated_entity, exp @@ -326,11 +331,11 @@ 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) -> str: + def update_trust_attestation(self, entity_id: str, attestation: list[str], exp: datetime, trust_type: TrustType, jwks: dict) -> str: old_entity = self._get_trust_attestation( "trust_attestations", entity_id) or {} upd_entity = self._update_attestation_metadata( - old_entity, attestation, exp, trust_type) + old_entity, attestation, exp, trust_type, jwks) return self._update_trust_attestation("trust_attestations", entity_id, upd_entity) diff --git a/pyeudiw/tests/satosa/test_backend.py b/pyeudiw/tests/satosa/test_backend.py index 95fa752a..6562c710 100644 --- a/pyeudiw/tests/satosa/test_backend.py +++ b/pyeudiw/tests/satosa/test_backend.py @@ -24,6 +24,7 @@ load_specification_from_yaml_string, import_ec ) +from pyeudiw.storage.base_storage import TrustType from pyeudiw.storage.db_engine import DBEngine from pyeudiw.tests.federation.base import ( trust_chain_wallet, @@ -58,6 +59,12 @@ def create_backend(self): exp=EXP, ) + issuer_jwk = leaf_cred_jwk_prot.serialize(private=True) + db_engine_inst.add_or_update_trust_attestation( + entity_id=CREDENTIAL_ISSUER_ENTITY_ID, + trust_type=TrustType.DIRECT_TRUST_SD_JWT_VC, + jwks=issuer_jwk) + self.backend = OpenID4VPBackend( Mock(), INTERNAL_ATTRIBUTES, CONFIG, BASE_URL, "name") @@ -259,7 +266,7 @@ def test_vp_validation_in_response_endpoint(self, context): assert request_endpoint.status == "400" msg = json.loads(request_endpoint.message) assert msg["error"] == "invalid_request" - assert msg["error_description"] == "DirectPostResponse content parse and validation error. Single VPs are faulty." + assert msg["error_description"] def test_response_endpoint(self, context): self.backend.register_endpoints() diff --git a/pyeudiw/tests/satosa/test_backend_trust.py b/pyeudiw/tests/satosa/test_backend_trust.py index 7d240e79..175d1a1a 100644 --- a/pyeudiw/tests/satosa/test_backend_trust.py +++ b/pyeudiw/tests/satosa/test_backend_trust.py @@ -6,7 +6,7 @@ from pyeudiw.tests.settings import ( BASE_URL, - CONFIG_DIRECT_TRUST, + CONFIG, INTERNAL_ATTRIBUTES, ) @@ -18,7 +18,7 @@ def setup_direct_trust(self): self.backend = OpenID4VPBackend( Mock(), INTERNAL_ATTRIBUTES, - CONFIG_DIRECT_TRUST, + CONFIG, BASE_URL, "name" ) diff --git a/pyeudiw/tests/sd_jwt/test_sdjwt.py b/pyeudiw/tests/sd_jwt/test_sdjwt.py index 0afbf976..fecc4d90 100644 --- a/pyeudiw/tests/sd_jwt/test_sdjwt.py +++ b/pyeudiw/tests/sd_jwt/test_sdjwt.py @@ -101,8 +101,8 @@ DISCLOSURES = [ "WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd", - "WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRy", \ - "ZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9u", \ + "WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRy" + + "ZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9u" + "IjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0", "WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd", "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0", diff --git a/pyeudiw/tests/settings.py b/pyeudiw/tests/settings.py index 3a9a847d..541fe83d 100644 --- a/pyeudiw/tests/settings.py +++ b/pyeudiw/tests/settings.py @@ -60,9 +60,9 @@ "httpc_params": httpc_params }, "trust": { - "direct_trust": { - "module": "pyeudiw.trust.default.federation", - "class": "FederationTrustModel", + "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": { @@ -365,10 +365,10 @@ CREDENTIAL_ISSUER_ENTITY_ID = "https://issuer.example.com" MODULE_DIRECT_TRUST_CONFIG = { - "module": "pyeudiw.trust.default.direct_trust", + "module": "pyeudiw.trust.default.direct_trust_sd_jwt_vc", "class": "DirectTrustSdJwtVc", "config": { - "endpoint": "/.well-known/jwt-vc-issuer", + "jwk_endpoint": "/.well-known/jwt-vc-issuer", "httpc_params": { "connection": { "ssl": True @@ -425,7 +425,7 @@ "httpc_params": httpc_params }, "trust": { - "direct_trust": MODULE_DIRECT_TRUST_CONFIG + "direct_trust_sd_jwt_vc": MODULE_DIRECT_TRUST_CONFIG }, "metadata_jwks": [ { diff --git a/pyeudiw/tests/trust/test_dynamic.py b/pyeudiw/tests/trust/test_dynamic.py index 94436c99..bce0ac2c 100644 --- a/pyeudiw/tests/trust/test_dynamic.py +++ b/pyeudiw/tests/trust/test_dynamic.py @@ -43,8 +43,8 @@ def test_trust_evaluators_loader(): "class": "MockTrustEvaluator", "config": {} }, - "direct_trust": { - "module": "pyeudiw.trust.default.direct_trust", + "direct_trust_sd_jwt_vc": { + "module": "pyeudiw.trust.default.direct_trust_sd_jwt_vc", "class": "DirectTrustSdJwtVc", "config": { "jwk_endpoint": "/.well-known/jwt-vc-issuer", @@ -62,14 +62,14 @@ def test_trust_evaluators_loader(): trust_sources = dynamic_trust_evaluators_loader(config) assert "mock" in trust_sources assert trust_sources["mock"].__class__.__name__ == "MockTrustEvaluator" - assert "direct_trust" in trust_sources - assert trust_sources["direct_trust"].__class__.__name__ == "DirectTrustSdJwtVc" + assert "direct_trust_sd_jwt_vc" in trust_sources + assert trust_sources["direct_trust_sd_jwt_vc"].__class__.__name__ == "DirectTrustSdJwtVc" def test_combined_trust_evaluator(): evaluators = { "mock": MockTrustEvaluator(), - "direct_trust": DirectTrustSdJwtVc(**DEFAULT_DIRECT_TRUST_PARAMS) + "direct_trust_sd_jwt_vc": DirectTrustSdJwtVc(**DEFAULT_DIRECT_TRUST_PARAMS) } combined = CombinedTrustEvaluator(evaluators) assert MockTrustEvaluator.mock_jwk in combined.get_public_keys("mock_issuer") diff --git a/pyeudiw/trust/dynamic.py b/pyeudiw/trust/dynamic.py index 5c0d4183..7233ef22 100644 --- a/pyeudiw/trust/dynamic.py +++ b/pyeudiw/trust/dynamic.py @@ -1,5 +1,7 @@ from typing import Any, Optional, 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 @@ -25,7 +27,7 @@ def dynamic_trust_evaluators_loader(trust_config: dict[str, TrustModuleConfigura trust_instances: dict[str, TrustEvaluator] = {} if not trust_config: _package_logger.warning("no configured trust model, using direct trust model") - trust_instances["direct_trust"] = default_trust_evaluator() + trust_instances["direct_trust_sd_jwt_vc"] = default_trust_evaluator() return trust_instances for trust_model_name, trust_module_config in trust_config.items(): @@ -47,12 +49,31 @@ class CombinedTrustEvaluator(TrustEvaluator, BaseLogger): trust sources are queried when some metadata or key material is requested. """ - def __init__(self, trust_evaluators: dict[str, TrustEvaluator], storage: Optional[Any] = None): + def __init__(self, trust_evaluators: dict[str, TrustEvaluator], storage: Optional[DBEngine] = None): self.trust_evaluators: dict[str, TrustEvaluator] = trust_evaluators - self.storage = storage + self.storage: DBEngine | None = storage def _get_trust_identifier_names(self) -> str: return '['+','.join(self.trust_evaluators.keys())+']' + + def _get_public_keys_from_storage(self, eval_identifier: str, issuer: str) -> dict | None: + 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? + 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]: """ @@ -63,16 +84,15 @@ def get_public_keys(self, issuer: str) -> list[dict]: """ pks: list[dict] = [] for eval_identifier, eval_instance in self.trust_evaluators.items(): - # TODO: search in storage if key for (issuer, eval_identifier) exists and is live try: - new_pks = eval_instance.get_public_keys(issuer) + new_pks = self._get_public_keys(eval_identifier, eval_instance, 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 += new_pks + pks.append(new_pks) if not pks: - raise Exception(f"no trust evaluator can provide cyptographic matrerial for {issuer}: searched among: {self._get_trust_identifier_names()}") + raise Exception(f"no trust evaluator can provide cyptographic material for {issuer}: searched among: {self._get_trust_identifier_names()}") return pks def get_metadata(self, issuer: str) -> dict: diff --git a/pyeudiw/vci/utils.py b/pyeudiw/vci/utils.py index 4932d31c..6901bd94 100644 --- a/pyeudiw/vci/utils.py +++ b/pyeudiw/vci/utils.py @@ -7,7 +7,7 @@ def final_issuer_endpoint(issuer: str, wk_endpoint: str) -> str: - """Prepend the wk_endpoint part tot he path of the issuer. + """Prepend the wk_endpoint part to the path of the issuer. For example, if the issuer is 'https://example.com/tenant/1234' and the well known endpoint is '/.well-known/jwt-vc-issuer', then the final endpoint will be @@ -24,5 +24,6 @@ def cacheable_get_http_url(ttl_cache: int, urls: list[str] | str, httpc_params: """ wraps method 'get_http_url' around a ttl cache """ + # explicitly delete dummy argument ttl_cache since it is only needed for caching del ttl_cache return get_http_url(urls, httpc_params, http_async)