From 764b19f6aaa6c63246dab8989311b8cbbb6dceda Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Thu, 7 Dec 2023 10:34:36 +0100 Subject: [PATCH 01/19] fix: define a custom error for unsupported key while encrypting --- pyeudiw/jwt/__init__.py | 4 +++- pyeudiw/jwt/exceptions.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyeudiw/jwt/__init__.py b/pyeudiw/jwt/__init__.py index ed9661e3..17d7696e 100644 --- a/pyeudiw/jwt/__init__.py +++ b/pyeudiw/jwt/__init__.py @@ -12,6 +12,7 @@ from pyeudiw.jwk import JWK from pyeudiw.jwk.exceptions import KidError +from pyeudiw.jwt.exceptions import JWEEncryptionError from pyeudiw.jwt.utils import unpad_jwt_header DEFAULT_HASH_FUNC = "SHA-256" @@ -43,11 +44,12 @@ def __init__(self, jwk: JWK): def encrypt(self, plain_dict: Union[dict, str, int, None], **kwargs) -> str: _key = key_from_jwk_dict(self.jwk.as_dict()) - if isinstance(_key, cryptojwt.jwk.rsa.RSAKey): JWE_CLASS = JWE_RSA elif isinstance(_key, cryptojwt.jwk.ec.ECKey): JWE_CLASS = JWE_EC + else: + raise JWEEncryptionError(f"Error while encrypting: f{_key.__class__.__name__} not supported!") _payload: str | int | bytes = "" diff --git a/pyeudiw/jwt/exceptions.py b/pyeudiw/jwt/exceptions.py index 2e059616..6e9e73e4 100644 --- a/pyeudiw/jwt/exceptions.py +++ b/pyeudiw/jwt/exceptions.py @@ -1,2 +1,5 @@ class JWEDecryptionError(Exception): pass + +class JWEEncryptionError(Exception): + pass From 5a83c2b38f565b3be46b9c24e92761b447befe70 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Thu, 7 Dec 2023 12:43:42 +0100 Subject: [PATCH 02/19] fix: JWE encryption with EC key --- pyeudiw/jwt/__init__.py | 4 ++-- pyeudiw/tests/test_jwt.py | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pyeudiw/jwt/__init__.py b/pyeudiw/jwt/__init__.py index 17d7696e..ed79d342 100644 --- a/pyeudiw/jwt/__init__.py +++ b/pyeudiw/jwt/__init__.py @@ -71,8 +71,8 @@ def encrypt(self, plain_dict: Union[dict, str, int, None], **kwargs) -> str: ) if _key.kty == 'EC': - # TODO - TypeError: key must be bytes-like - return _keyobj.encrypt(cek=_key.public_key()) + cek, encrypted_key, iv, params, epk = _keyobj.enc_setup(_payload, key=_key) + return _keyobj.encrypt(cek=cek) else: return _keyobj.encrypt(key=_key.public_key()) diff --git a/pyeudiw/tests/test_jwt.py b/pyeudiw/tests/test_jwt.py index 0e8771a9..00ed54cc 100644 --- a/pyeudiw/tests/test_jwt.py +++ b/pyeudiw/tests/test_jwt.py @@ -19,9 +19,6 @@ JWKs = JWKs_EC + JWKs_RSA -# TODO: ENC also with EC and not only with RSA -ENC_JWKs = JWKs_RSA - @pytest.mark.parametrize("jwk, payload", JWKs_RSA) def test_unpad_jwt_header(jwk, payload): @@ -42,7 +39,7 @@ def test_jwe_helper_init(key_type): assert helper.jwk == jwk -@pytest.mark.parametrize("jwk, payload", ENC_JWKs) +@pytest.mark.parametrize("jwk, payload", JWKs) def test_jwe_helper_encrypt(jwk, payload): helper = JWEHelper(jwk) jwe = helper.encrypt(payload) @@ -60,7 +57,7 @@ def test_jwe_helper_decrypt(jwk, payload): assert decrypted == payload or decrypted == payload.encode() -@pytest.mark.parametrize("jwk, payload", ENC_JWKs) +@pytest.mark.parametrize("jwk, payload", JWKs) def test_jwe_helper_decrypt_fail(jwk, payload): helper = JWEHelper(jwk) jwe = helper.encrypt(payload) From 705c08c6feea988fa808bde39bf36d8870cbb436 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Thu, 7 Dec 2023 15:24:41 +0100 Subject: [PATCH 03/19] fix: JWE decryption with EC key --- pyeudiw/jwt/__init__.py | 14 +++++++++++--- pyeudiw/tests/test_jwt.py | 10 +++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/pyeudiw/jwt/__init__.py b/pyeudiw/jwt/__init__.py index ed79d342..a888869d 100644 --- a/pyeudiw/jwt/__init__.py +++ b/pyeudiw/jwt/__init__.py @@ -71,8 +71,10 @@ def encrypt(self, plain_dict: Union[dict, str, int, None], **kwargs) -> str: ) if _key.kty == 'EC': - cek, encrypted_key, iv, params, epk = _keyobj.enc_setup(_payload, key=_key) - return _keyobj.encrypt(cek=cek) + _keyobj: JWE_EC + cek, encrypted_key, iv, params, epk = _keyobj.enc_setup(msg=_payload, key=_key) + kwargs = {"params": params, "cek": cek, "iv": iv, "encrypted_key": encrypted_key} + return _keyobj.encrypt(**kwargs) else: return _keyobj.encrypt(key=_key.public_key()) @@ -89,7 +91,13 @@ def decrypt(self, jwe: str) -> dict: _decryptor = factory(jwe, alg=_alg, enc=_enc) _dkey = key_from_jwk_dict(self.jwk.as_dict()) - msg = _decryptor.decrypt(jwe, [_dkey]) + + if isinstance(_dkey, cryptojwt.jwk.ec.ECKey): + jwdec = JWE_EC() + jwdec.dec_setup(_decryptor.jwt, key=self.jwk.key.private_key()) + msg = jwdec.decrypt(_decryptor.jwt) + else: + msg = _decryptor.decrypt(jwe, [_dkey]) try: msg_dict = json.loads(msg) diff --git a/pyeudiw/tests/test_jwt.py b/pyeudiw/tests/test_jwt.py index 00ed54cc..1679cfc1 100644 --- a/pyeudiw/tests/test_jwt.py +++ b/pyeudiw/tests/test_jwt.py @@ -20,7 +20,7 @@ JWKs = JWKs_EC + JWKs_RSA -@pytest.mark.parametrize("jwk, payload", JWKs_RSA) +@pytest.mark.parametrize("jwk, payload", JWKs) def test_unpad_jwt_header(jwk, payload): jwe_helper = JWEHelper(jwk) jwe = jwe_helper.encrypt(payload) @@ -46,7 +46,7 @@ def test_jwe_helper_encrypt(jwk, payload): assert jwe -@pytest.mark.parametrize("jwk, payload", JWKs_RSA) +@pytest.mark.parametrize("jwk, payload", JWKs) def test_jwe_helper_decrypt(jwk, payload): helper = JWEHelper(jwk) jwe = helper.encrypt(payload) @@ -74,14 +74,14 @@ def test_jws_helper_init(key_type): assert helper.jwk == jwk -@pytest.mark.parametrize("jwk, payload", JWKs_RSA) +@pytest.mark.parametrize("jwk, payload", JWKs) def test_jws_helper_sign(jwk, payload): helper = JWSHelper(jwk) jws = helper.sign(payload) assert jws -@pytest.mark.parametrize("jwk, payload", JWKs_RSA) +@pytest.mark.parametrize("jwk, payload", JWKs) def test_jws_helper_verify(jwk, payload): helper = JWSHelper(jwk) jws = helper.sign(payload) @@ -92,7 +92,7 @@ def test_jws_helper_verify(jwk, payload): assert verified == payload or verified == payload.encode() -@pytest.mark.parametrize("jwk, payload", JWKs_RSA) +@pytest.mark.parametrize("jwk, payload", JWKs) def test_jws_helper_verify_fail(jwk, payload): helper = JWSHelper(jwk) jws = helper.sign(payload) From 9a9b60592a70c0184290f2ccc9731e9ec91c3fbe Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Mon, 11 Dec 2023 14:33:23 +0100 Subject: [PATCH 04/19] feat: adapt EC keys --- pyeudiw/sd_jwt/__init__.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/pyeudiw/sd_jwt/__init__.py b/pyeudiw/sd_jwt/__init__.py index 14cd1d89..618d6386 100644 --- a/pyeudiw/sd_jwt/__init__.py +++ b/pyeudiw/sd_jwt/__init__.py @@ -105,12 +105,29 @@ def import_pyca_pri_rsa(key, **params): ) return jwcrypto.jwk.JWK(**params) +def import_ec(key, **params): + pn = key.private_numbers() + params.update( + kty="EC", + crv="P-256", + x=pk_encode_int(pn.public_numbers.x), + y=pk_encode_int(pn.public_numbers.y), + d=pk_encode_int(pn.private_value) + ) + return jwcrypto.jwk.JWK(**params) def _adapt_keys(issuer_key: JWK, holder_key: JWK): # _iss_key = issuer_key.key.serialize(private=True) # _iss_key['key_ops'] = 'sign' - _issuer_key = import_pyca_pri_rsa( - issuer_key.key.priv_key, kid=issuer_key.kid) + + match issuer_key.jwk["kty"]: + case "RSA": + _issuer_key = import_pyca_pri_rsa( + issuer_key.key.priv_key, kid=issuer_key.kid) + case "EC": + _issuer_key = import_ec(issuer_key.key.priv_key, kid=issuer_key.kid) + case _: + raise KeyError(f"Unsupported 'kty' {issuer_key.key['kty']}") holder_key = jwcrypto.jwk.JWK.from_json( json.dumps(_serialize_key(holder_key))) @@ -136,7 +153,6 @@ def issue_sd_jwt(specification: dict, settings: dict, issuer_key: JWK, holder_ke specification.update(claims) use_decoys = specification.get("add_decoy_claims", True) adapted_keys = _adapt_keys(issuer_key, holder_key) - additional_headers = {"trust_chain": trust_chain} if trust_chain else {} additional_headers['kid'] = issuer_key.kid From a2518f1628a8a889f7de233c60731dcd3981b193 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Mon, 11 Dec 2023 14:33:36 +0100 Subject: [PATCH 05/19] test: integration test with EC keys --- example/satosa/integration_test/main_ec.py | 283 ++++++++++++++++++ .../satosa/integration_test/settings_ec.py | 135 +++++++++ pyeudiw/tests/federation/base_ec.py | 218 ++++++++++++++ 3 files changed, 636 insertions(+) create mode 100644 example/satosa/integration_test/main_ec.py create mode 100644 example/satosa/integration_test/settings_ec.py create mode 100644 pyeudiw/tests/federation/base_ec.py diff --git a/example/satosa/integration_test/main_ec.py b/example/satosa/integration_test/main_ec.py new file mode 100644 index 00000000..7c5c56c9 --- /dev/null +++ b/example/satosa/integration_test/main_ec.py @@ -0,0 +1,283 @@ +import requests +import uuid +import urllib +import datetime +import base64 +from bs4 import BeautifulSoup + +from pyeudiw.jwt import DEFAULT_SIG_KTY_MAP +from pyeudiw.tests.federation.base_ec import ( + EXP, + leaf_cred, + leaf_cred_jwk, + leaf_wallet_jwk, + leaf_wallet, + leaf_wallet_signed, + trust_chain_issuer, + trust_chain_wallet, + ta_ec, + ta_ec_signed, + leaf_cred_signed, leaf_cred_jwk_prot +) + +from pyeudiw.jwk import JWK +from pyeudiw.jwt import JWSHelper, JWEHelper +from pyeudiw.oauth2.dpop import DPoPIssuer, DPoPVerifier +from pyeudiw.sd_jwt import ( + load_specification_from_yaml_string, + issue_sd_jwt, + _adapt_keys, + import_pyca_pri_rsa, import_ec +) +from pyeudiw.storage.db_engine import DBEngine +from pyeudiw.jwt.utils import unpad_jwt_payload +from pyeudiw.tools.utils import iat_now, exp_from_now + +from saml2_sp import saml2_request, IDP_BASEURL +from sd_jwt.holder import SDJWTHolder + +from settings_ec import ( + CONFIG_DB, + RP_EID, + WALLET_INSTANCE_ATTESTATION, + its_trust_chain +) + +# put a trust attestation related itself into the storage +# this then is used as trust_chain header paramenter in the signed +# request object +db_engine_inst = DBEngine(CONFIG_DB) + +# STORAGE #### +db_engine_inst.add_trust_anchor( + entity_id = ta_ec['iss'], + entity_configuration = ta_ec_signed, + exp=EXP +) + +db_engine_inst.add_or_update_trust_attestation( + entity_id=RP_EID, + attestation=its_trust_chain, + exp=datetime.datetime.now().isoformat() +) + +db_engine_inst.add_or_update_trust_attestation( + entity_id=leaf_wallet['iss'], + attestation=leaf_wallet_signed, + exp=datetime.datetime.now().isoformat() +) + +db_engine_inst.add_or_update_trust_attestation( + entity_id=leaf_cred['iss'], + attestation=leaf_cred_signed, + exp=datetime.datetime.now().isoformat() +) + +req_url = f"{saml2_request['headers'][0][1]}&idp_hinting=wallet" +headers_mobile = { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B137 Safari/601.1' +} + +request_uri = '' + +# initialize the user-agent +http_user_agent = requests.Session() + +try: + authn_response = http_user_agent.get( + url=req_url, + verify=False, + headers=headers_mobile + ) +except requests.exceptions.InvalidSchema as e: + request_uri = urllib.parse.unquote_plus( + e.args[0].split("request_uri=" + )[1][:-1] + ) + +WALLET_PRIVATE_JWK = JWK(leaf_wallet_jwk.serialize(private=True)) +WALLET_PUBLIC_JWK = JWK(leaf_wallet_jwk.serialize()) +jwshelper = JWSHelper(WALLET_PRIVATE_JWK) +dpop_wia = jwshelper.sign( + WALLET_INSTANCE_ATTESTATION, + protected={ + 'trust_chain': trust_chain_wallet, + 'typ': "va+jwt" + } +) + +dpop_proof = DPoPIssuer( + htu=request_uri, + token=dpop_wia, + private_jwk=WALLET_PRIVATE_JWK +).proof +dpop_test = DPoPVerifier( + public_jwk=leaf_wallet_jwk.serialize(), + http_header_authz=f"DPoP {dpop_wia}", + http_header_dpop=dpop_proof +) +print(f"dpop is valid: {dpop_test.is_valid}") + +http_headers = { + "AUTHORIZATION": f"DPoP {dpop_wia}", + "DPOP": dpop_proof +} + +sign_request_obj = http_user_agent.get( + request_uri, verify=False, headers=http_headers) +print(sign_request_obj.json()) + +redirect_uri = unpad_jwt_payload(sign_request_obj.json()['response'])[ + 'response_uri'] + +# create a SD-JWT signed by a trusted credential issuer +issuer_jwk = leaf_cred_jwk +ISSUER_CONF = { + "sd_specification": """ + user_claims: + !sd unique_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + !sd given_name: "Mario" + !sd family_name: "Rossi" + !sd birthdate: "1980-01-10" + !sd place_of_birth: + country: "IT" + locality: "Rome" + !sd tax_id_code: "TINIT-XXXXXXXXXXXXXXXX" + + holder_disclosed_claims: + { "given_name": "Mario", "family_name": "Rossi", "place_of_birth": {country: "IT", locality: "Rome"}, "tax_id_code": "TINIT-XXXXXXXXXXXXXXXX" } + + key_binding: True + """, + "issuer": leaf_cred['sub'], + "default_exp": 1024 +} +settings = ISSUER_CONF +settings['issuer'] = leaf_cred['iss'] +settings['default_exp'] = 33 + +sd_specification = load_specification_from_yaml_string( + settings["sd_specification"] +) + +ISSUER_PRIVATE_JWK = JWK(leaf_cred_jwk.serialize(private=True)) + +CREDENTIAL_ISSUER_JWK = JWK(leaf_cred_jwk_prot.serialize(private=True)) + +issued_jwt = issue_sd_jwt( + sd_specification, + settings, + CREDENTIAL_ISSUER_JWK, + WALLET_PUBLIC_JWK, + trust_chain=trust_chain_issuer +) + +adapted_keys = _adapt_keys( + issuer_key=ISSUER_PRIVATE_JWK, + holder_key=WALLET_PUBLIC_JWK, +) + +sdjwt_at_holder = SDJWTHolder( + issued_jwt["issuance"], + serialization_format="compact", +) +sdjwt_at_holder.create_presentation( + claims_to_disclose={ + 'tax_id_code': "TIN-that", + 'given_name': 'Raffaello', + 'family_name': 'Mascetti' + }, + nonce=str(uuid.uuid4()), + aud=str(uuid.uuid4()), + sign_alg=DEFAULT_SIG_KTY_MAP[WALLET_PRIVATE_JWK.key.kty], + holder_key=( + import_ec( + WALLET_PRIVATE_JWK.key.priv_key, + kid=WALLET_PRIVATE_JWK.kid + ) + if sd_specification.get("key_binding", False) + else None + ) +) + +red_data = unpad_jwt_payload(sign_request_obj.json()['response']) +req_nonce = red_data['nonce'] + +data = { + "iss": "https://wallet-provider.example.org/instance/vbeXJksM45xphtANnCiG6mCyuU4jfGNzopGuKvogg9c", + "jti": str(uuid.uuid4()), + "aud": "https://relying-party.example.org/callback", + "iat": iat_now(), + "exp": exp_from_now(minutes=5), + "nonce": req_nonce, + "vp": sdjwt_at_holder.sd_jwt_presentation, +} + +vp_token = JWSHelper(WALLET_PRIVATE_JWK).sign( + data, + protected={"typ": "JWT"} +) + +# take relevant information from RP's EC +rp_ec_jwt = http_user_agent.get( + f'{IDP_BASEURL}/OpenID4VP/.well-known/openid-federation', + verify=False +).content.decode() +rp_ec = unpad_jwt_payload(rp_ec_jwt) + +assert redirect_uri == rp_ec["metadata"]['wallet_relying_party']["redirect_uris"][0] + +response = { + "state": red_data['state'], + "nonce": req_nonce, + "vp_token": vp_token, + "presentation_submission": { + "definition_id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "id": "04a98be3-7fb0-4cf5-af9a-31579c8b0e7d", + "descriptor_map": [ + { + "id": "pid-sd-jwt:unique_id+given_name+family_name", + "path": "$.vp_token.verified_claims.claims._sd[0]", + "format": "vc+sd-jwt" + } + ], + "aud": redirect_uri + } +} +encrypted_response = JWEHelper( + # RSA (EC is not fully supported todate) + JWK(rp_ec["metadata"]['wallet_relying_party']['jwks']['keys'][1]) +).encrypt(response) + + +sign_request_obj = http_user_agent.post( + redirect_uri, + verify=False, + data={'response': encrypted_response} +) + +assert 'SAMLResponse' in sign_request_obj.content.decode() +print(sign_request_obj.content.decode()) + +soup = BeautifulSoup(sign_request_obj.content.decode(), features="lxml") +form = soup.find("form") +assert "/saml2" in form["action"] +input_tag = soup.find("input") +assert input_tag["name"] == "SAMLResponse" +value = BeautifulSoup(base64.b64decode(input_tag["value"]), features="xml") +attributes = value.find_all("saml:attribute") + + +expected = { + # https://oidref.com/2.5.4.42 + "urn:oid:2.5.4.42": ISSUER_CONF['sd_specification'].split('!sd given_name:')[1].split('"')[1], + # https://oidref.com/2.5.4.4 + "urn:oid:2.5.4.4": ISSUER_CONF['sd_specification'].split('!sd family_name:')[1].split('"')[1] +} + +for attribute in attributes: + name = attribute["name"] + value = attribute.contents[0].contents[0] + expected_value = expected.get(name, None) + if expected_value: + assert value == expected_value diff --git a/example/satosa/integration_test/settings_ec.py b/example/satosa/integration_test/settings_ec.py new file mode 100644 index 00000000..484eba91 --- /dev/null +++ b/example/satosa/integration_test/settings_ec.py @@ -0,0 +1,135 @@ + +from cryptojwt.jws.jws import JWS +from cryptojwt.jwk.jwk import key_from_jwk_dict +from pyeudiw.tests.federation.base_ec import ( + NOW, + EXP, + leaf_wallet_jwk, + ta_ec, + ta_jwk +) + +from pyeudiw.tools.utils import iat_now, exp_from_now + + +RP_EID = "https://localhost:10000/OpenID4VP" + +CONFIG_DB = { + "mongo_db": { + "storage": { + "module": "pyeudiw.storage.mongo_storage", + "class": "MongoStorage", + "init_params": { + "url": "mongodb://localhost:27017/", + "conf": { + "db_name": "eudiw", + "db_sessions_collection": "sessions", + "db_trust_attestations_collection": "trust_attestations", + "db_trust_anchors_collection": "trust_anchors" + }, + "connection_params": {} + } + } + } +} + + +WALLET_INSTANCE_ATTESTATION = { + "iss": "https://wallet-provider.example.org", + "sub": "vbeXJksM45xphtANnCiG6mCyuU4jfGNzopGuKvogg9c", + "type": "WalletInstanceAttestation", + "policy_uri": "https://wallet-provider.example.org/privacy_policy", + "tos_uri": "https://wallet-provider.example.org/info_policy", + "logo_uri": "https://wallet-provider.example.org/logo.svg", + "asc": "https://wallet-provider.example.org/LoA/basic", + "cnf": + { + "jwk": leaf_wallet_jwk.serialize() + }, + "authorization_endpoint": "eudiw:", + "response_types_supported": [ + "vp_token" + ], + "vp_formats_supported": { + "jwt_vp_json": { + "alg_values_supported": ["ES256"] + }, + "jwt_vc_json": { + "alg_values_supported": ["ES256"] + } + }, + "request_object_signing_alg_values_supported": [ + "ES256" + ], + "presentation_definition_uri_supported": False, + "iat": iat_now(), + "exp": exp_from_now() +} + +rp_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" + }, + { + 'kty': 'EC', + 'kid': 'xPFTWxeGHTVTaDlzGad0MKN5JmWOSnRqEjJCtvQpoyg', + 'crv': 'P-256', + 'x': 'EkMoe7qPLGMydWO_evC3AXEeXJlLQk9tNRkYcpp7xHo', + 'y': 'VLoHFl90D1SdTTjMvNf3WssWiCBXcU1lGNPbOmcCqdU', + 'd': 'oGzjgBbIYNL9opdJ_rDPnCJF89yN8yj8wegdkYfaxw0' + } +] +rp_ec = { + "exp": EXP, + "iat": NOW, + "iss": RP_EID, + "sub": RP_EID, + 'jwks': {"keys": rp_jwks}, + "metadata": { + "wallet_relying_party": { + 'jwks': {"keys": []} + }, + "federation_entity": { + "organization_name": "OpenID Wallet Verifier example", + "homepage_uri": "https://verifier.example.org/home", + "policy_uri": "https://verifier.example.org/policy", + "logo_uri": "https://verifier.example.org/static/logo.svg", + "contacts": [ + "tech@verifier.example.org" + ] + } + }, + "authority_hints": [ + ta_ec['iss'] + ] +} +rp_signer = JWS( + rp_ec, alg="ES256", + typ="application/entity-statement+jwt" +) + + +_es = ta_es = { + "exp": EXP, + "iat": NOW, + "iss": ta_ec['iss'], + "sub": RP_EID, + 'jwks': { + 'keys': rp_jwks + } +} +ta_signer = JWS( + _es, alg="ES256", + typ="application/entity-statement+jwt" +) + +its_trust_chain = [ + rp_signer.sign_compact([key_from_jwk_dict(rp_jwks[1])]), + ta_signer.sign_compact([ta_jwk]) +] diff --git a/pyeudiw/tests/federation/base_ec.py b/pyeudiw/tests/federation/base_ec.py new file mode 100644 index 00000000..625e1d9d --- /dev/null +++ b/pyeudiw/tests/federation/base_ec.py @@ -0,0 +1,218 @@ +from cryptojwt.jwk.ec import new_ec_key +from cryptojwt.jws.jws import JWS + +import json +import pyeudiw.federation.trust_chain_validator as tcv_test +from pyeudiw.tools.utils import iat_now, exp_from_now + +httpc_params = { + "connection": {"ssl": True}, + "session": {"timeout": 6}, +} + +NOW = iat_now() +EXP = exp_from_now(5000) + +ec_crv = "P-256" +ec_alg = "ES256" + +# Define intermediate ec +intermediate_jwk = new_ec_key(ec_crv, alg=ec_alg) + +# Define TA ec +ta_jwk = new_ec_key(ec_crv, alg=ec_alg) + +# Define leaf Credential Issuer +leaf_cred_jwk = new_ec_key(ec_crv, alg=ec_alg) +leaf_cred_jwk_prot = new_ec_key(ec_crv, alg=ec_alg) +leaf_cred = { + "exp": EXP, + "iat": NOW, + "iss": "https://credential_issuer.example.org", + "sub": "https://credential_issuer.example.org", + 'jwks': {"keys": []}, + "metadata": { + "openid_credential_issuer": { + 'jwks': {"keys": []} + }, + "federation_entity": { + "organization_name": "OpenID Credential Issuer example", + "homepage_uri": "https://credential_issuer.example.org/home", + "policy_uri": "https://credential_issuer.example.org/policy", + "logo_uri": "https://credential_issuer.example.org/static/logo.svg", + "contacts": [ + "tech@credential_issuer.example.org" + ] + } + }, + "authority_hints": [ + "https://intermediate.eidas.example.org" + ] +} +leaf_cred['jwks']['keys'] = [leaf_cred_jwk.serialize()] +leaf_cred['metadata']['openid_credential_issuer']['jwks']['keys'] = [ + leaf_cred_jwk_prot.serialize()] + + +# Define intermediate Entity Statement for credential +intermediate_es_cred = { + "exp": EXP, + "iat": NOW, + "iss": "https://intermediate.eidas.example.org", + "sub": "https://credential_issuer.example.org", + 'jwks': {"keys": []} +} +intermediate_es_cred["jwks"]['keys'] = [leaf_cred_jwk.serialize()] + +# Define leaf Wallet Provider +leaf_wallet_jwk = new_ec_key(ec_crv, alg=ec_alg) +leaf_wallet = { + "exp": EXP, + "iat": NOW, + "iss": "https://wallet-provider.example.org", + "sub": "https://wallet-provider.example.org", + 'jwks': {"keys": []}, + "metadata": { + "wallet_provider": { + "jwks": {"keys": []} + }, + "federation_entity": { + "organization_name": "OpenID Wallet Verifier example", + "homepage_uri": "https://wallet-provider.example.org/home", + "policy_uri": "https://wallet-provider.example.org/policy", + "logo_uri": "https://wallet-provider.example.org/static/logo.svg", + "contacts": [ + "tech@wallet-provider.example.org" + ] + } + }, + "authority_hints": [ + "https://intermediate.eidas.example.org" + ] +} +leaf_wallet['jwks']['keys'] = [leaf_wallet_jwk.serialize()] +leaf_wallet['metadata']['wallet_provider'] = [leaf_wallet_jwk.serialize()] + +# Define intermediate Entity Statement for wallet provider +intermediate_es_wallet = { + "exp": EXP, + "iat": NOW, + "iss": "https://intermediate.eidas.example.org", + "sub": "https://wallet-provider.example.org", + 'jwks': {"keys": [leaf_wallet_jwk.serialize()]} +} + +# Intermediate EC +intermediate_ec = { + "exp": EXP, + "iat": NOW, + 'iss': 'https://intermediate.eidas.example.org', + 'sub': 'https://intermediate.eidas.example.org', + 'jwks': {"keys": [intermediate_jwk.serialize()]}, + 'metadata': { + 'federation_entity': { + 'contacts': ['soggetto@intermediate.eidas.example.it'], + 'federation_fetch_endpoint': 'https://intermediate.eidas.example.org/fetch', + 'federation_resolve_endpoint': 'https://intermediate.eidas.example.org/resolve', + 'federation_list_endpoint': 'https://intermediate.eidas.example.org/list', + 'homepage_uri': 'https://soggetto.intermediate.eidas.example.it', + 'name': 'Example Intermediate intermediate.eidas.example' + } + }, + "authority_hints": [ + "https://trust-anchor.example.org" + ] +} + + +# Define TA +ta_es = { + "exp": EXP, + "iat": NOW, + "iss": "https://trust-anchor.example.org", + "sub": "https://intermediate.eidas.example.org", + 'jwks': {"keys": [intermediate_jwk.serialize()]} +} + +ta_ec = { + "exp": EXP, + "iat": NOW, + "iss": "https://trust-anchor.example.org", + "sub": "https://trust-anchor.example.org", + 'jwks': {"keys": [ta_jwk.serialize()]}, + "metadata": { + "federation_entity": { + 'federation_fetch_endpoint': 'https://trust-anchor.example.org/fetch', + 'federation_resolve_endpoint': 'https://trust-anchor.example.org/resolve', + 'federation_list_endpoint': 'https://trust-anchor.example.org/list', + "organization_name": "TA example", + "homepage_uri": "https://trust-anchor.example.org/home", + "policy_uri": "https://trust-anchor.example.org/policy", + "logo_uri": "https://trust-anchor.example.org/static/logo.svg", + "contacts": [ + "tech@trust-anchor.example.org" + ] + } + }, + 'constraints': {'max_path_length': 1} +} + +# Sign step +leaf_cred_signer = JWS(leaf_cred, alg=ec_alg, + typ='entity-statement+jwt') +leaf_cred_signed = leaf_cred_signer.sign_compact([leaf_cred_jwk]) + +leaf_wallet_signer = JWS(leaf_wallet, alg=ec_alg, + typ='entity-statement+jwt') +leaf_wallet_signed = leaf_wallet_signer.sign_compact([leaf_wallet_jwk]) + + +intermediate_signer_ec = JWS( + intermediate_ec, alg=ec_alg, + typ="entity-statement+jwt" +) +intermediate_ec_signed = intermediate_signer_ec.sign_compact([ + intermediate_jwk]) + + +intermediate_signer_es_cred = JWS( + intermediate_es_cred, alg=ec_alg, typ='entity-statement+jwt') +intermediate_es_cred_signed = intermediate_signer_es_cred.sign_compact([ + intermediate_jwk]) + +intermediate_signer_es_wallet = JWS( + intermediate_es_wallet, alg=ec_alg, typ='entity-statement+jwt') +intermediate_es_wallet_signed = intermediate_signer_es_wallet.sign_compact([ + intermediate_jwk]) + +ta_es_signer = JWS(ta_es, alg=ec_alg, typ="entity-statement+jwt") +ta_es_signed = ta_es_signer.sign_compact([ta_jwk]) + +ta_ec_signer = JWS(ta_ec, alg=ec_alg, typ="entity-statement+jwt") +ta_ec_signed = ta_ec_signer.sign_compact([ta_jwk]) + + +trust_chain_issuer = [ + leaf_cred_signed, + intermediate_es_cred_signed, + ta_es_signed, + ta_ec_signed +] + +trust_chain_wallet = [ + leaf_wallet_signed, + intermediate_es_wallet_signed, + ta_es_signed +] + +test_cred = tcv_test.StaticTrustChainValidator( + trust_chain_issuer, [ta_jwk.serialize()], httpc_params=httpc_params +) +assert test_cred.is_valid + +test_wallet = tcv_test.StaticTrustChainValidator( + trust_chain_wallet, [ta_jwk.serialize()], httpc_params=httpc_params +) +assert test_wallet.is_valid + +print(json.dumps(trust_chain_issuer, indent=2)) From 96abf606e13bc37b2363686292e327e21e6e676e Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Mon, 11 Dec 2023 14:51:21 +0100 Subject: [PATCH 06/19] feat: dynamic JWK schema loading --- pyeudiw/jwk/schemas/__init__.py | 0 pyeudiw/jwk/schemas/jwk.py | 24 ++++++++++++++++++++++++ pyeudiw/oauth2/dpop/schema.py | 5 +++-- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 pyeudiw/jwk/schemas/__init__.py create mode 100644 pyeudiw/jwk/schemas/jwk.py diff --git a/pyeudiw/jwk/schemas/__init__.py b/pyeudiw/jwk/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyeudiw/jwk/schemas/jwk.py b/pyeudiw/jwk/schemas/jwk.py new file mode 100644 index 00000000..1aa11513 --- /dev/null +++ b/pyeudiw/jwk/schemas/jwk.py @@ -0,0 +1,24 @@ +from typing import Literal, Annotated, Union, Optional + +from pydantic import BaseModel, Field + + +class JwkBaseModel(BaseModel): + use: Optional[Literal["sig", "enc"]] = None + kid: Optional[str] = None + + +class RSAJwkSchema(JwkBaseModel): + kty: Literal["RSA"] + n: str + e: str + + +class ECJwkSchema(JwkBaseModel): + kty: Literal["EC"] + crv: Literal["P-256", "P-384", "P-521"] + x: str + y: str + + +JwkSchema = Annotated[Union[ECJwkSchema, RSAJwkSchema], Field(discriminator="kty")] diff --git a/pyeudiw/oauth2/dpop/schema.py b/pyeudiw/oauth2/dpop/schema.py index 2674fbfb..22ad3c57 100644 --- a/pyeudiw/oauth2/dpop/schema.py +++ b/pyeudiw/oauth2/dpop/schema.py @@ -2,6 +2,8 @@ from pydantic import BaseModel, HttpUrl +from pyeudiw.jwk.schemas.jwk import JwkSchema + class DPoPTokenHeaderSchema(BaseModel): # header @@ -17,8 +19,7 @@ class DPoPTokenHeaderSchema(BaseModel): "PS384", "PS512", ] - # TODO - dynamic schemas loader if EC or RSA - # jwk: JwkSchema + jwk: JwkSchema class DPoPTokenPayloadSchema(BaseModel): From 53baa7dcde330254858865473fa8063359357bc9 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Mon, 11 Dec 2023 15:12:32 +0100 Subject: [PATCH 07/19] test: dynamic JWK schema loading This will provide a useful example on how to use the dynamic schema as per https://github.com/italia/eudi-wallet-it-python/issues/102#issuecomment-1689928198 --- pyeudiw/tests/test_jwk.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pyeudiw/tests/test_jwk.py b/pyeudiw/tests/test_jwk.py index 77f6f391..264d4576 100644 --- a/pyeudiw/tests/test_jwk.py +++ b/pyeudiw/tests/test_jwk.py @@ -1,6 +1,8 @@ import pytest +from pydantic import TypeAdapter from pyeudiw.jwk import JWK +from pyeudiw.jwk.schemas.jwk import JwkSchema, ECJwkSchema, RSAJwkSchema @pytest.mark.parametrize( @@ -50,3 +52,18 @@ def test_export_public_pem(): jwk_public_pem = jwk.export_public_pem() assert jwk_public_pem assert "BEGIN PUBLIC KEY" in jwk_public_pem + + +@pytest.mark.parametrize("key_type", ["EC", "RSA"]) +def test_dynamic_schema_validation(key_type): + jwk = JWK(key_type=key_type) + model = TypeAdapter(JwkSchema).validate_python(jwk.as_dict()) + match key_type: + case "EC": + assert isinstance(model, ECJwkSchema) + assert not isinstance(model, RSAJwkSchema) + case "RSA": + assert isinstance(model, RSAJwkSchema) + assert not isinstance(model, ECJwkSchema) + + From 96da976b918cbf6ee6c7887914f7f8fc69097342 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Tue, 12 Dec 2023 14:51:52 +0100 Subject: [PATCH 08/19] fix: imports --- example/satosa/integration_test/main.py | 8 ++++---- example/satosa/integration_test/main_ec.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/example/satosa/integration_test/main.py b/example/satosa/integration_test/main.py index a4459d4d..66d0e731 100644 --- a/example/satosa/integration_test/main.py +++ b/example/satosa/integration_test/main.py @@ -30,7 +30,7 @@ import_pyca_pri_rsa ) from pyeudiw.storage.db_engine import DBEngine -from pyeudiw.jwt.utils import unpad_jwt_payload +from pyeudiw.jwt.utils import decode_jwt_payload from pyeudiw.tools.utils import iat_now, exp_from_now from saml2_sp import saml2_request, IDP_BASEURL @@ -127,7 +127,7 @@ request_uri, verify=False, headers=http_headers) print(sign_request_obj.json()) -redirect_uri = unpad_jwt_payload(sign_request_obj.json()['response'])[ +redirect_uri = decode_jwt_payload(sign_request_obj.json()['response'])[ 'response_uri'] # create a SD-JWT signed by a trusted credential issuer @@ -200,7 +200,7 @@ ) ) -red_data = unpad_jwt_payload(sign_request_obj.json()['response']) +red_data = decode_jwt_payload(sign_request_obj.json()['response']) req_nonce = red_data['nonce'] data = { @@ -223,7 +223,7 @@ f'{IDP_BASEURL}/OpenID4VP/.well-known/openid-federation', verify=False ).content.decode() -rp_ec = unpad_jwt_payload(rp_ec_jwt) +rp_ec = decode_jwt_payload(rp_ec_jwt) assert redirect_uri == rp_ec["metadata"]['wallet_relying_party']["redirect_uris"][0] diff --git a/example/satosa/integration_test/main_ec.py b/example/satosa/integration_test/main_ec.py index 7c5c56c9..b2bf5c13 100644 --- a/example/satosa/integration_test/main_ec.py +++ b/example/satosa/integration_test/main_ec.py @@ -30,7 +30,7 @@ import_pyca_pri_rsa, import_ec ) from pyeudiw.storage.db_engine import DBEngine -from pyeudiw.jwt.utils import unpad_jwt_payload +from pyeudiw.jwt.utils import decode_jwt_payload from pyeudiw.tools.utils import iat_now, exp_from_now from saml2_sp import saml2_request, IDP_BASEURL @@ -127,7 +127,7 @@ request_uri, verify=False, headers=http_headers) print(sign_request_obj.json()) -redirect_uri = unpad_jwt_payload(sign_request_obj.json()['response'])[ +redirect_uri = decode_jwt_payload(sign_request_obj.json()['response'])[ 'response_uri'] # create a SD-JWT signed by a trusted credential issuer @@ -200,7 +200,7 @@ ) ) -red_data = unpad_jwt_payload(sign_request_obj.json()['response']) +red_data = decode_jwt_payload(sign_request_obj.json()['response']) req_nonce = red_data['nonce'] data = { @@ -223,7 +223,7 @@ f'{IDP_BASEURL}/OpenID4VP/.well-known/openid-federation', verify=False ).content.decode() -rp_ec = unpad_jwt_payload(rp_ec_jwt) +rp_ec = decode_jwt_payload(rp_ec_jwt) assert redirect_uri == rp_ec["metadata"]['wallet_relying_party']["redirect_uris"][0] From ae37a3d13cc4c4b3dd0826d6f8026c73eb51c903 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Tue, 12 Dec 2023 15:33:51 +0100 Subject: [PATCH 09/19] chore: keep only EC keys based integration test --- example/satosa/integration_test/main.py | 8 +- example/satosa/integration_test/main_ec.py | 283 --------------------- 2 files changed, 4 insertions(+), 287 deletions(-) delete mode 100644 example/satosa/integration_test/main_ec.py diff --git a/example/satosa/integration_test/main.py b/example/satosa/integration_test/main.py index 66d0e731..20aa4543 100644 --- a/example/satosa/integration_test/main.py +++ b/example/satosa/integration_test/main.py @@ -6,7 +6,7 @@ from bs4 import BeautifulSoup from pyeudiw.jwt import DEFAULT_SIG_KTY_MAP -from pyeudiw.tests.federation.base import ( +from pyeudiw.tests.federation.base_ec import ( EXP, leaf_cred, leaf_cred_jwk, @@ -27,7 +27,7 @@ load_specification_from_yaml_string, issue_sd_jwt, _adapt_keys, - import_pyca_pri_rsa + import_ec ) from pyeudiw.storage.db_engine import DBEngine from pyeudiw.jwt.utils import decode_jwt_payload @@ -36,7 +36,7 @@ from saml2_sp import saml2_request, IDP_BASEURL from sd_jwt.holder import SDJWTHolder -from settings import ( +from settings_ec import ( CONFIG_DB, RP_EID, WALLET_INSTANCE_ATTESTATION, @@ -191,7 +191,7 @@ aud=str(uuid.uuid4()), sign_alg=DEFAULT_SIG_KTY_MAP[WALLET_PRIVATE_JWK.key.kty], holder_key=( - import_pyca_pri_rsa( + import_ec( WALLET_PRIVATE_JWK.key.priv_key, kid=WALLET_PRIVATE_JWK.kid ) diff --git a/example/satosa/integration_test/main_ec.py b/example/satosa/integration_test/main_ec.py deleted file mode 100644 index b2bf5c13..00000000 --- a/example/satosa/integration_test/main_ec.py +++ /dev/null @@ -1,283 +0,0 @@ -import requests -import uuid -import urllib -import datetime -import base64 -from bs4 import BeautifulSoup - -from pyeudiw.jwt import DEFAULT_SIG_KTY_MAP -from pyeudiw.tests.federation.base_ec import ( - EXP, - leaf_cred, - leaf_cred_jwk, - leaf_wallet_jwk, - leaf_wallet, - leaf_wallet_signed, - trust_chain_issuer, - trust_chain_wallet, - ta_ec, - ta_ec_signed, - leaf_cred_signed, leaf_cred_jwk_prot -) - -from pyeudiw.jwk import JWK -from pyeudiw.jwt import JWSHelper, JWEHelper -from pyeudiw.oauth2.dpop import DPoPIssuer, DPoPVerifier -from pyeudiw.sd_jwt import ( - load_specification_from_yaml_string, - issue_sd_jwt, - _adapt_keys, - import_pyca_pri_rsa, import_ec -) -from pyeudiw.storage.db_engine import DBEngine -from pyeudiw.jwt.utils import decode_jwt_payload -from pyeudiw.tools.utils import iat_now, exp_from_now - -from saml2_sp import saml2_request, IDP_BASEURL -from sd_jwt.holder import SDJWTHolder - -from settings_ec import ( - CONFIG_DB, - RP_EID, - WALLET_INSTANCE_ATTESTATION, - its_trust_chain -) - -# put a trust attestation related itself into the storage -# this then is used as trust_chain header paramenter in the signed -# request object -db_engine_inst = DBEngine(CONFIG_DB) - -# STORAGE #### -db_engine_inst.add_trust_anchor( - entity_id = ta_ec['iss'], - entity_configuration = ta_ec_signed, - exp=EXP -) - -db_engine_inst.add_or_update_trust_attestation( - entity_id=RP_EID, - attestation=its_trust_chain, - exp=datetime.datetime.now().isoformat() -) - -db_engine_inst.add_or_update_trust_attestation( - entity_id=leaf_wallet['iss'], - attestation=leaf_wallet_signed, - exp=datetime.datetime.now().isoformat() -) - -db_engine_inst.add_or_update_trust_attestation( - entity_id=leaf_cred['iss'], - attestation=leaf_cred_signed, - exp=datetime.datetime.now().isoformat() -) - -req_url = f"{saml2_request['headers'][0][1]}&idp_hinting=wallet" -headers_mobile = { - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B137 Safari/601.1' -} - -request_uri = '' - -# initialize the user-agent -http_user_agent = requests.Session() - -try: - authn_response = http_user_agent.get( - url=req_url, - verify=False, - headers=headers_mobile - ) -except requests.exceptions.InvalidSchema as e: - request_uri = urllib.parse.unquote_plus( - e.args[0].split("request_uri=" - )[1][:-1] - ) - -WALLET_PRIVATE_JWK = JWK(leaf_wallet_jwk.serialize(private=True)) -WALLET_PUBLIC_JWK = JWK(leaf_wallet_jwk.serialize()) -jwshelper = JWSHelper(WALLET_PRIVATE_JWK) -dpop_wia = jwshelper.sign( - WALLET_INSTANCE_ATTESTATION, - protected={ - 'trust_chain': trust_chain_wallet, - 'typ': "va+jwt" - } -) - -dpop_proof = DPoPIssuer( - htu=request_uri, - token=dpop_wia, - private_jwk=WALLET_PRIVATE_JWK -).proof -dpop_test = DPoPVerifier( - public_jwk=leaf_wallet_jwk.serialize(), - http_header_authz=f"DPoP {dpop_wia}", - http_header_dpop=dpop_proof -) -print(f"dpop is valid: {dpop_test.is_valid}") - -http_headers = { - "AUTHORIZATION": f"DPoP {dpop_wia}", - "DPOP": dpop_proof -} - -sign_request_obj = http_user_agent.get( - request_uri, verify=False, headers=http_headers) -print(sign_request_obj.json()) - -redirect_uri = decode_jwt_payload(sign_request_obj.json()['response'])[ - 'response_uri'] - -# create a SD-JWT signed by a trusted credential issuer -issuer_jwk = leaf_cred_jwk -ISSUER_CONF = { - "sd_specification": """ - user_claims: - !sd unique_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - !sd given_name: "Mario" - !sd family_name: "Rossi" - !sd birthdate: "1980-01-10" - !sd place_of_birth: - country: "IT" - locality: "Rome" - !sd tax_id_code: "TINIT-XXXXXXXXXXXXXXXX" - - holder_disclosed_claims: - { "given_name": "Mario", "family_name": "Rossi", "place_of_birth": {country: "IT", locality: "Rome"}, "tax_id_code": "TINIT-XXXXXXXXXXXXXXXX" } - - key_binding: True - """, - "issuer": leaf_cred['sub'], - "default_exp": 1024 -} -settings = ISSUER_CONF -settings['issuer'] = leaf_cred['iss'] -settings['default_exp'] = 33 - -sd_specification = load_specification_from_yaml_string( - settings["sd_specification"] -) - -ISSUER_PRIVATE_JWK = JWK(leaf_cred_jwk.serialize(private=True)) - -CREDENTIAL_ISSUER_JWK = JWK(leaf_cred_jwk_prot.serialize(private=True)) - -issued_jwt = issue_sd_jwt( - sd_specification, - settings, - CREDENTIAL_ISSUER_JWK, - WALLET_PUBLIC_JWK, - trust_chain=trust_chain_issuer -) - -adapted_keys = _adapt_keys( - issuer_key=ISSUER_PRIVATE_JWK, - holder_key=WALLET_PUBLIC_JWK, -) - -sdjwt_at_holder = SDJWTHolder( - issued_jwt["issuance"], - serialization_format="compact", -) -sdjwt_at_holder.create_presentation( - claims_to_disclose={ - 'tax_id_code': "TIN-that", - 'given_name': 'Raffaello', - 'family_name': 'Mascetti' - }, - nonce=str(uuid.uuid4()), - aud=str(uuid.uuid4()), - sign_alg=DEFAULT_SIG_KTY_MAP[WALLET_PRIVATE_JWK.key.kty], - holder_key=( - import_ec( - WALLET_PRIVATE_JWK.key.priv_key, - kid=WALLET_PRIVATE_JWK.kid - ) - if sd_specification.get("key_binding", False) - else None - ) -) - -red_data = decode_jwt_payload(sign_request_obj.json()['response']) -req_nonce = red_data['nonce'] - -data = { - "iss": "https://wallet-provider.example.org/instance/vbeXJksM45xphtANnCiG6mCyuU4jfGNzopGuKvogg9c", - "jti": str(uuid.uuid4()), - "aud": "https://relying-party.example.org/callback", - "iat": iat_now(), - "exp": exp_from_now(minutes=5), - "nonce": req_nonce, - "vp": sdjwt_at_holder.sd_jwt_presentation, -} - -vp_token = JWSHelper(WALLET_PRIVATE_JWK).sign( - data, - protected={"typ": "JWT"} -) - -# take relevant information from RP's EC -rp_ec_jwt = http_user_agent.get( - f'{IDP_BASEURL}/OpenID4VP/.well-known/openid-federation', - verify=False -).content.decode() -rp_ec = decode_jwt_payload(rp_ec_jwt) - -assert redirect_uri == rp_ec["metadata"]['wallet_relying_party']["redirect_uris"][0] - -response = { - "state": red_data['state'], - "nonce": req_nonce, - "vp_token": vp_token, - "presentation_submission": { - "definition_id": "32f54163-7166-48f1-93d8-ff217bdb0653", - "id": "04a98be3-7fb0-4cf5-af9a-31579c8b0e7d", - "descriptor_map": [ - { - "id": "pid-sd-jwt:unique_id+given_name+family_name", - "path": "$.vp_token.verified_claims.claims._sd[0]", - "format": "vc+sd-jwt" - } - ], - "aud": redirect_uri - } -} -encrypted_response = JWEHelper( - # RSA (EC is not fully supported todate) - JWK(rp_ec["metadata"]['wallet_relying_party']['jwks']['keys'][1]) -).encrypt(response) - - -sign_request_obj = http_user_agent.post( - redirect_uri, - verify=False, - data={'response': encrypted_response} -) - -assert 'SAMLResponse' in sign_request_obj.content.decode() -print(sign_request_obj.content.decode()) - -soup = BeautifulSoup(sign_request_obj.content.decode(), features="lxml") -form = soup.find("form") -assert "/saml2" in form["action"] -input_tag = soup.find("input") -assert input_tag["name"] == "SAMLResponse" -value = BeautifulSoup(base64.b64decode(input_tag["value"]), features="xml") -attributes = value.find_all("saml:attribute") - - -expected = { - # https://oidref.com/2.5.4.42 - "urn:oid:2.5.4.42": ISSUER_CONF['sd_specification'].split('!sd given_name:')[1].split('"')[1], - # https://oidref.com/2.5.4.4 - "urn:oid:2.5.4.4": ISSUER_CONF['sd_specification'].split('!sd family_name:')[1].split('"')[1] -} - -for attribute in attributes: - name = attribute["name"] - value = attribute.contents[0].contents[0] - expected_value = expected.get(name, None) - if expected_value: - assert value == expected_value From 5132b72760b37d58fc79ca27ff25537d77b4f181 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Tue, 12 Dec 2023 15:54:34 +0100 Subject: [PATCH 10/19] fix: elliptic curve name support --- pyeudiw/sd_jwt/__init__.py | 13 ++++++++++++- pyeudiw/sd_jwt/exceptions.py | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 pyeudiw/sd_jwt/exceptions.py diff --git a/pyeudiw/sd_jwt/__init__.py b/pyeudiw/sd_jwt/__init__.py index d418de08..b6049022 100644 --- a/pyeudiw/sd_jwt/__init__.py +++ b/pyeudiw/sd_jwt/__init__.py @@ -14,6 +14,7 @@ from pyeudiw.jwk import JWK from pyeudiw.jwt import DEFAULT_SIG_KTY_MAP from pyeudiw.jwt.utils import decode_jwt_payload +from pyeudiw.sd_jwt.exceptions import UnknownCurveNistName from pyeudiw.tools.utils import exp_from_now, iat_now from jwcrypto.jws import JWS @@ -107,9 +108,19 @@ def import_pyca_pri_rsa(key, **params): def import_ec(key, **params): pn = key.private_numbers() + curve_name = key.curve.name + match curve_name: + case "secp256r1": + nist_name = "P-256" + case "secp384r1": + nist_name = "P-384" + case "secp512r1": + nist_name = "P-512" + case _: + raise UnknownCurveNistName(f"Cannot translate {key.curve.name} into NIST name.") params.update( kty="EC", - crv="P-256", + crv=nist_name, x=pk_encode_int(pn.public_numbers.x), y=pk_encode_int(pn.public_numbers.y), d=pk_encode_int(pn.private_value) diff --git a/pyeudiw/sd_jwt/exceptions.py b/pyeudiw/sd_jwt/exceptions.py new file mode 100644 index 00000000..d46299db --- /dev/null +++ b/pyeudiw/sd_jwt/exceptions.py @@ -0,0 +1,2 @@ +class UnknownCurveNistName(Exception): + pass From 8c1979f5a5559e87612ee1d38e5ae8291783d0ee Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Thu, 14 Dec 2023 10:36:04 +0100 Subject: [PATCH 11/19] fix: remove redundant schema --- .../schemas/oid4vc_presentation_definition.py | 4 - .../schemas/presentation_definition.py | 29 ---- .../schemas/test_presentation_definition.py | 155 +----------------- ...definition_for_a_high_assurance_profile.py | 13 -- 4 files changed, 8 insertions(+), 193 deletions(-) delete mode 100644 pyeudiw/presentation_exchange/schemas/presentation_definition.py delete mode 100644 pyeudiw/tests/presentation_exchange/schemas/test_presentation_definition_for_a_high_assurance_profile.py diff --git a/pyeudiw/presentation_exchange/schemas/oid4vc_presentation_definition.py b/pyeudiw/presentation_exchange/schemas/oid4vc_presentation_definition.py index 74bcb116..ab287aef 100644 --- a/pyeudiw/presentation_exchange/schemas/oid4vc_presentation_definition.py +++ b/pyeudiw/presentation_exchange/schemas/oid4vc_presentation_definition.py @@ -114,7 +114,3 @@ class PresentationDefinition(BaseModel): id: str input_descriptors: List[InputDescriptor] submission_requirements: Optional[List[SubmissionRequirement]] = None - - -class PresentationDefinitionForAHighAssuranceProfile(BaseModel): - presentation_definition: Optional[PresentationDefinition] = None diff --git a/pyeudiw/presentation_exchange/schemas/presentation_definition.py b/pyeudiw/presentation_exchange/schemas/presentation_definition.py deleted file mode 100644 index aa064554..00000000 --- a/pyeudiw/presentation_exchange/schemas/presentation_definition.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Any, Dict, List, Optional - -from pydantic import BaseModel - - -class InputDescriptorJwt(BaseModel): - alg: List[str] - - -class MsoMdoc(BaseModel): - alg: List[str] - - -class FormatSchema(BaseModel): - jwt: Optional[InputDescriptorJwt] = None - mso_mdoc: Optional[MsoMdoc] = None - constraints: Optional[Dict[str, Any]] = None - - -class InputDescriptor(BaseModel): - id: str - name: Optional[str] = None - purpose: Optional[str] = None - format: Optional[str | FormatSchema] = None - - -class PresentationDefinition(BaseModel): - id: str - input_descriptors: List[InputDescriptor] diff --git a/pyeudiw/tests/presentation_exchange/schemas/test_presentation_definition.py b/pyeudiw/tests/presentation_exchange/schemas/test_presentation_definition.py index 766782ca..40d874ee 100644 --- a/pyeudiw/tests/presentation_exchange/schemas/test_presentation_definition.py +++ b/pyeudiw/tests/presentation_exchange/schemas/test_presentation_definition.py @@ -1,151 +1,12 @@ -import pytest -from pydantic import ValidationError +import json +from pathlib import Path -from pyeudiw.presentation_exchange.schemas.presentation_definition import PresentationDefinition, InputDescriptor - -PID_SD_JWT = { - "id": "pid-sd-jwt:unique_id+given_name+family_name", - "input_descriptors": [ - { - "id": "sd-jwt", - "format": { - "jwt": { - "alg": [ - "EdDSA", - "ES256" - ] - }, - "constraints": { - "limit_disclosure": "required", - "fields": [ - { - "path": [ - "$.sd-jwt.type" - ], - "filter": { - "type": "string", - "const": "PersonIdentificationData" - } - }, - { - "path": [ - "$.sd-jwt.cnf" - ], - "filter": { - "type": "object", - } - }, - { - "path": [ - "$.sd-jwt.family_name" - ], - "intent_to_retain": "true" - }, - { - "path": [ - "$.sd-jwt.given_name" - ], - "intent_to_retain": "true" - }, - { - "path": [ - "$.sd-jwt.unique_id" - ], - "intent_to_retain": "true" - } - ] - } - } - } - ] -} - -MDL_SAMPLE_REQ = { - "id": "mDL-sample-req", - "input_descriptors": [ - { - "id": "mDL", - "format": { - "mso_mdoc": { - "alg": [ - "EdDSA", - "ES256" - ] - }, - "constraints": { - "limit_disclosure": "required", - "fields": [ - { - "path": [ - "$.mdoc.doctype" - ], - "filter": { - "type": "string", - "const": "org.iso.18013.5.1.mDL" - } - }, - { - "path": [ - "$.mdoc.namespace" - ], - "filter": { - "type": "string", - "const": "org.iso.18013.5.1" - } - }, - { - "path": [ - "$.mdoc.family_name" - ], - "intent_to_retain": "false" - }, - { - "path": [ - "$.mdoc.portrait" - ], - "intent_to_retain": "false" - }, - { - "path": [ - "$.mdoc.driving_privileges" - ], - "intent_to_retain": "false" - } - ] - } - } - } - ] -} - - -def test_input_descriptor(): - descriptor = PID_SD_JWT["input_descriptors"][0] - InputDescriptor(**descriptor) - descriptor["format"]["jwt"]["alg"] = "ES256" - with pytest.raises(ValidationError): - InputDescriptor(**descriptor) - descriptor["format"]["jwt"]["alg"] = ["ES256"] +from pyeudiw.presentation_exchange.schemas.oid4vc_presentation_definition import \ + PresentationDefinition def test_presentation_definition(): - PresentationDefinition(**PID_SD_JWT) - PresentationDefinition(**MDL_SAMPLE_REQ) - - PID_SD_JWT["input_descriptors"][0]["format"]["jwt"]["alg"] = "ES256" - with pytest.raises(ValidationError): - PresentationDefinition(**PID_SD_JWT) - - PID_SD_JWT["input_descriptors"][0]["format"]["jwt"]["alg"] = ["ES256"] - PresentationDefinition(**PID_SD_JWT) - - del PID_SD_JWT["input_descriptors"][0]["format"]["jwt"]["alg"] - # alg is an emtpy dict, which is not allowed - with pytest.raises(ValidationError): - PresentationDefinition(**PID_SD_JWT) - - del PID_SD_JWT["input_descriptors"][0]["format"]["jwt"] - # since jwt is Optional, this is allowed - PresentationDefinition(**PID_SD_JWT) - - PID_SD_JWT["input_descriptors"][0]["format"]["jwt"] = {"alg": ["ES256"]} + p = Path(__file__).with_name('presentation_definition_sd_jwt_vc.json') + with open(p) as json_file: + data = json.load(json_file) + PresentationDefinition(**data) diff --git a/pyeudiw/tests/presentation_exchange/schemas/test_presentation_definition_for_a_high_assurance_profile.py b/pyeudiw/tests/presentation_exchange/schemas/test_presentation_definition_for_a_high_assurance_profile.py deleted file mode 100644 index 258f1ddb..00000000 --- a/pyeudiw/tests/presentation_exchange/schemas/test_presentation_definition_for_a_high_assurance_profile.py +++ /dev/null @@ -1,13 +0,0 @@ -import json -from pathlib import Path - -from pyeudiw.presentation_exchange.schemas.oid4vc_presentation_definition import \ - PresentationDefinition - - -def test_presentation_definition(): - p = Path(__file__).with_name('presentation_definition_sd_jwt_vc.json') - with open(p) as json_file: - data = json.load(json_file) - PresentationDefinition(**data) - From 0c52d923cb98c0191530ae9c48503c1c8fc24289 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Thu, 14 Dec 2023 14:51:07 +0100 Subject: [PATCH 12/19] fix: update integration test to handle uppercase chars --- example/satosa/integration_test/main.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/example/satosa/integration_test/main.py b/example/satosa/integration_test/main.py index 20aa4543..ba2c7881 100644 --- a/example/satosa/integration_test/main.py +++ b/example/satosa/integration_test/main.py @@ -264,9 +264,12 @@ assert "/saml2" in form["action"] input_tag = soup.find("input") assert input_tag["name"] == "SAMLResponse" -value = BeautifulSoup(base64.b64decode(input_tag["value"]), features="xml") -attributes = value.find_all("saml:attribute") +lowered = base64.b64decode(input_tag["value"]).lower() +value = BeautifulSoup(lowered, features="xml") +attributes = value.find_all("saml:attribute") +# expect to have a non-empty list of attributes +assert attributes expected = { # https://oidref.com/2.5.4.42 @@ -280,4 +283,4 @@ value = attribute.contents[0].contents[0] expected_value = expected.get(name, None) if expected_value: - assert value == expected_value + assert value == expected_value.lower() From 98c24ffb690796865641093fdb1e1a7195c9d1b3 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Fri, 15 Dec 2023 12:23:00 +0100 Subject: [PATCH 13/19] fix: presentation definition validation in integration test --- example/satosa/integration_test/main.py | 4 ++ example/satosa/pyeudiw_backend.yaml | 75 ++++++------------------- 2 files changed, 21 insertions(+), 58 deletions(-) diff --git a/example/satosa/integration_test/main.py b/example/satosa/integration_test/main.py index ba2c7881..f99af9dd 100644 --- a/example/satosa/integration_test/main.py +++ b/example/satosa/integration_test/main.py @@ -6,6 +6,7 @@ from bs4 import BeautifulSoup from pyeudiw.jwt import DEFAULT_SIG_KTY_MAP +from pyeudiw.presentation_exchange.schemas.oid4vc_presentation_definition import PresentationDefinition from pyeudiw.tests.federation.base_ec import ( EXP, leaf_cred, @@ -225,6 +226,9 @@ ).content.decode() rp_ec = decode_jwt_payload(rp_ec_jwt) +presentation_definition = rp_ec["metadata"]["wallet_relying_party"]["presentation_definition"] +PresentationDefinition(**presentation_definition) + assert redirect_uri == rp_ec["metadata"]['wallet_relying_party']["redirect_uris"][0] response = { diff --git a/example/satosa/pyeudiw_backend.yaml b/example/satosa/pyeudiw_backend.yaml index d1d9e091..cb59c126 100644 --- a/example/satosa/pyeudiw_backend.yaml +++ b/example/satosa/pyeudiw_backend.yaml @@ -168,66 +168,25 @@ config: # jwks: #This section contains the details for presentation request - presentation_definitions: - - id: pid-sd-jwt:unique_id+given_name+family_name - input_descriptors: - - id: pid-sd-jwt:unique_id+given_name+family_name + presentation_definition: + id: d76c51b7-ea90-49bb-8368-6b3d194fc131 + input_descriptors: + - id: IdentityCredential format: - constraints: - fields: - - filter: - const: PersonIdentificationData + vc+sd-jwt: { } + constraints: + limit_disclosure: required + fields: + - path: + - "$.vct" + filter: type: string - path: - - $.sd-jwt.type - - filter: - type: object - path: - - $.sd-jwt.cnf - - intent_to_retain: 'true' - path: - - $.sd-jwt.family_name - - intent_to_retain: 'true' - path: - - $.sd-jwt.given_name - - intent_to_retain: 'true' - path: - - $.sd-jwt.unique_id - limit_disclosure: required - jwt: - alg: - - EdDSA - - ES256 - - id: mDL-sample-req - input_descriptors: - - format: - constraints: - fields: - - filter: - const: org.iso.18013.5.1.mDL - type: string - path: - - $.mdoc.doctype - - filter: - const: org.iso.18013.5.1 - type: string - path: - - $.mdoc.namespace - - intent_to_retain: 'false' - path: - - $.mdoc.family_name - - intent_to_retain: 'false' - path: - - $.mdoc.portrait - - intent_to_retain: 'false' - path: - - $.mdoc.driving_privileges - limit_disclosure: required - mso_mdoc: - alg: - - EdDSA - - ES256 - id: mDL + const: IdentityCredential + - path: + - "$.family_name" + - path: + - "$.given_name" + redirect_uris: - //redirect-uri From 53e1c5ce30db3d229007dcdb120792de45abe8b6 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Fri, 15 Dec 2023 16:48:35 +0100 Subject: [PATCH 14/19] fix: remove port 10000 --- example/satosa/integration_test/metadata/idp.xml | 2 +- example/satosa/integration_test/saml2_sp.py | 2 +- example/satosa/integration_test/settings.py | 2 +- example/satosa/integration_test/settings_ec.py | 2 +- example/satosa/static/disco.html | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/example/satosa/integration_test/metadata/idp.xml b/example/satosa/integration_test/metadata/idp.xml index f5920160..332747ba 100644 --- a/example/satosa/integration_test/metadata/idp.xml +++ b/example/satosa/integration_test/metadata/idp.xml @@ -1 +1 @@ -change with $SATOSA_UI_DISPLAY_NAME_ENchange with $SATOSA_UI_DISPLAY_NAME_ITchange with $SATOSA_UI_DESCRIPTION_ENchange with $SATOSA_UI_DESCRIPTION_ITchange with $SATOSA_UI_LOGO_URLchange with $SATOSA_UI_INFORMATION_URL_ENchange with $SATOSA_UI_INFORMATION_URL_ITchange with $SATOSA_UI_PRIVACY_URL_ENchange with $SATOSA_UI_PRIVACY_URL_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:oasis:names:tc:SAML:2.0:nameid-format:transientchange with $SATOSA_ORGANIZATION_NAME_ENchange with $SATOSA_ORGANIZATION_NAME_ITchange with $SATOSA_ORGANIZATION_DISPLAY_NAME_ENchange with $SAOSA_ORGANIZATION_DISPLAY_NAME_ITchange with $SATOSA_ORGANIZATION_URL_ENchange with $SATOSA_ORGANIZATION_URL_ITchange with $SATOSA_CONTACT_PERSON_GIVEN_NAMEchange with $SATOSA_CONTACT_PERSON_EMAIL_ADDRESS \ No newline at end of file +change with $SATOSA_UI_DISPLAY_NAME_ENchange with $SATOSA_UI_DISPLAY_NAME_ITchange with $SATOSA_UI_DESCRIPTION_ENchange with $SATOSA_UI_DESCRIPTION_ITchange with $SATOSA_UI_LOGO_URLchange with $SATOSA_UI_INFORMATION_URL_ENchange with $SATOSA_UI_INFORMATION_URL_ITchange with $SATOSA_UI_PRIVACY_URL_ENchange with $SATOSA_UI_PRIVACY_URL_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:oasis:names:tc:SAML:2.0:nameid-format:transientchange with $SATOSA_ORGANIZATION_NAME_ENchange with $SATOSA_ORGANIZATION_NAME_ITchange with $SATOSA_ORGANIZATION_DISPLAY_NAME_ENchange with $SAOSA_ORGANIZATION_DISPLAY_NAME_ITchange with $SATOSA_ORGANIZATION_URL_ENchange with $SATOSA_ORGANIZATION_URL_ITchange with $SATOSA_CONTACT_PERSON_GIVEN_NAMEchange with $SATOSA_CONTACT_PERSON_EMAIL_ADDRESS \ No newline at end of file diff --git a/example/satosa/integration_test/saml2_sp.py b/example/satosa/integration_test/saml2_sp.py index dfa29564..134b24fc 100644 --- a/example/satosa/integration_test/saml2_sp.py +++ b/example/satosa/integration_test/saml2_sp.py @@ -16,7 +16,7 @@ BASE = 'http://pyeudiw_demo.example.org' BASE_URL = '{}/saml2'.format(BASE) -IDP_BASEURL = "https://localhost:10000" +IDP_BASEURL = "https://localhost" IDP_ENTITYID = f'{IDP_BASEURL}/Saml2IDP/metadata' SAML_CONFIG = { diff --git a/example/satosa/integration_test/settings.py b/example/satosa/integration_test/settings.py index e2a65f3e..ea20cf4c 100644 --- a/example/satosa/integration_test/settings.py +++ b/example/satosa/integration_test/settings.py @@ -12,7 +12,7 @@ from pyeudiw.tools.utils import iat_now, exp_from_now -RP_EID = "https://localhost:10000/OpenID4VP" +RP_EID = "https://localhost/OpenID4VP" CONFIG_DB = { "mongo_db": { diff --git a/example/satosa/integration_test/settings_ec.py b/example/satosa/integration_test/settings_ec.py index 484eba91..87b58886 100644 --- a/example/satosa/integration_test/settings_ec.py +++ b/example/satosa/integration_test/settings_ec.py @@ -12,7 +12,7 @@ from pyeudiw.tools.utils import iat_now, exp_from_now -RP_EID = "https://localhost:10000/OpenID4VP" +RP_EID = "https://localhost/OpenID4VP" CONFIG_DB = { "mongo_db": { diff --git a/example/satosa/static/disco.html b/example/satosa/static/disco.html index 33df2977..5f1afb47 100644 --- a/example/satosa/static/disco.html +++ b/example/satosa/static/disco.html @@ -39,7 +39,7 @@

IT Wallet

- Date: Mon, 18 Dec 2023 11:49:36 +0100 Subject: [PATCH 15/19] fix: validate the schema after init Move the validation at the end of the initialization since some fields are transformed by the `__init__` function rather than simply loaded. --- .../schemas/wallet_relying_party.py | 6 +++-- pyeudiw/jwk/schemas/jwk.py | 6 ++++- pyeudiw/satosa/backend.py | 24 ++++++++++--------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/pyeudiw/federation/schemas/wallet_relying_party.py b/pyeudiw/federation/schemas/wallet_relying_party.py index 0eb0cbe9..12f74d0f 100644 --- a/pyeudiw/federation/schemas/wallet_relying_party.py +++ b/pyeudiw/federation/schemas/wallet_relying_party.py @@ -1,8 +1,10 @@ from enum import Enum from typing import Any, List -from pyeudiw.jwk.schema import JwksSchema +from pyeudiw.jwk.schemas.jwk import JwksSchema from pydantic import BaseModel, HttpUrl, PositiveInt from pyeudiw.openid4vp.schemas import VPFormat +from pyeudiw.presentation_exchange.schemas.oid4vc_presentation_definition import PresentationDefinition + class AcrValuesSupported(str, Enum): spid_l1 = "https://www.spid.gov.it/SpidL1" @@ -46,7 +48,7 @@ class WalletRelyingParty(BaseModel): request_uris: List[HttpUrl] redirect_uris: List[HttpUrl] default_acr_values: List[HttpUrl] - presentation_definitions: List[Any] + presentation_definition: PresentationDefinition authorization_signed_response_alg: List[AuthorizationSignedResponseAlg] authorization_encrypted_response_alg: List[EncryptionAlgValuesSupported] authorization_encrypted_response_enc: List[EncryptionEncValuesSupported] diff --git a/pyeudiw/jwk/schemas/jwk.py b/pyeudiw/jwk/schemas/jwk.py index 1aa11513..4b5c1ae4 100644 --- a/pyeudiw/jwk/schemas/jwk.py +++ b/pyeudiw/jwk/schemas/jwk.py @@ -1,4 +1,4 @@ -from typing import Literal, Annotated, Union, Optional +from typing import Literal, Annotated, Union, Optional, List from pydantic import BaseModel, Field @@ -22,3 +22,7 @@ class ECJwkSchema(JwkBaseModel): JwkSchema = Annotated[Union[ECJwkSchema, RSAJwkSchema], Field(discriminator="kty")] + + +class JwksSchema(BaseModel): + keys: List[JwkSchema] diff --git a/pyeudiw/satosa/backend.py b/pyeudiw/satosa/backend.py index c8131646..008e7850 100644 --- a/pyeudiw/satosa/backend.py +++ b/pyeudiw/satosa/backend.py @@ -65,18 +65,7 @@ def __init__(self, auth_callback_func, internal_attributes, config, base_url, na """ super().__init__(auth_callback_func, internal_attributes, base_url, name) - try: - WalletRelyingParty(**config['metadata']) - except ValidationError as e: - logger.warning( - """ - The backend configuration presents the following validation issues: - {} - """.format(logger.warning(e))) - self.config = config - self.client_id = self.config['metadata']['client_id'] - self.default_exp = int(self.config['jwt']['default_exp']) self.metadata_jwks_by_kids = { i['kid']: i for i in self.config['metadata_jwks'] @@ -86,6 +75,10 @@ def __init__(self, auth_callback_func, internal_attributes, config, base_url, na JWK(i).public_key for i in self.config['metadata_jwks'] ]} + + self.client_id = self.config['metadata']['client_id'] + self.default_exp = int(self.config['jwt']['default_exp']) + # HTML template loader self.template = Jinja2TemplateHandler(self.config["ui"]) @@ -99,6 +92,15 @@ def __init__(self, auth_callback_func, internal_attributes, config, base_url, na self._render_metadata_conf_elements() self.init_trust_resources() + try: + WalletRelyingParty(**config['metadata']) + except ValidationError as e: + logger.warning( + """ + The backend configuration presents the following validation issues: + {} + """.format(logger.warning(e))) + logger.debug( lu.LOG_FMT.format( id="OpenID4VP init", From ab236436ecfd145972466a481869901cd12f41c2 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Mon, 18 Dec 2023 12:26:36 +0100 Subject: [PATCH 16/19] fix: extend valid authorization algs --- pyeudiw/federation/schemas/wallet_relying_party.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyeudiw/federation/schemas/wallet_relying_party.py b/pyeudiw/federation/schemas/wallet_relying_party.py index 12f74d0f..9d0e375b 100644 --- a/pyeudiw/federation/schemas/wallet_relying_party.py +++ b/pyeudiw/federation/schemas/wallet_relying_party.py @@ -37,7 +37,11 @@ class SigningAlgValuesSupported(str, Enum): class AuthorizationSignedResponseAlg(str, Enum): rs256 = "RS256" + rs384 = "RS384" + rs512 = "RS512" es256 = "ES256" + es384 = "ES384" + es512 = "ES512" class WalletRelyingParty(BaseModel): application_type: str From 5908a3a706583f67853011b8f4cdb5eec387c562 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Mon, 18 Dec 2023 12:37:36 +0100 Subject: [PATCH 17/19] fix: update presentation definition in examples --- .../schemas/test_entity_configuration.py | 134 ++++------------- pyeudiw/tests/federation/test_schema.py | 35 ++++- .../tests/openid4vp/schemas/test_schema.py | 138 ++++-------------- 3 files changed, 90 insertions(+), 217 deletions(-) diff --git a/pyeudiw/tests/federation/schemas/test_entity_configuration.py b/pyeudiw/tests/federation/schemas/test_entity_configuration.py index 5592d26d..22788490 100644 --- a/pyeudiw/tests/federation/schemas/test_entity_configuration.py +++ b/pyeudiw/tests/federation/schemas/test_entity_configuration.py @@ -66,121 +66,41 @@ ] } }, - "presentation_definitions": [ - { - "id": "pid-sd-jwt:unique_id+given_name+family_name", + "presentation_definition": { + "id": "d76c51b7-ea90-49bb-8368-6b3d194fc131", "input_descriptors": [ { - "id": "sd-jwt", + "id": "IdentityCredential", "format": { - "jwt": { - "alg": [ - "EdDSA", - "ES256" - ] - }, - "constraints": { - "limit_disclosure": "required", - "fields": [ - { - "path": [ - "$.sd-jwt.type" - ], - "filter": { - "type": "string", - "const": "PersonIdentificationData" - } - }, - { - "path": [ - "$.sd-jwt.cnf" - ], - "filter": { - "type": "object", - } - }, - { - "path": [ - "$.sd-jwt.family_name" - ], - "intent_to_retain": "true" - }, - { - "path": [ - "$.sd-jwt.given_name" - ], - "intent_to_retain": "true" - }, - { - "path": [ - "$.sd-jwt.unique_id" - ], - "intent_to_retain": "true" + "vc+sd-jwt": {} + }, + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": [ + "$.vct" + ], + "filter": { + "type": "string", + "const": "IdentityCredential" } - ] - } + }, + { + "path": [ + "$.family_name" + ] + }, + { + "path": [ + "$.given_name" + ] + } + ] } } ] }, - { - "id": "mDL-sample-req", - "input_descriptors": [ - { - "id": "mDL", - "format": { - "mso_mdoc": { - "alg": [ - "EdDSA", - "ES256" - ] - }, - "constraints": { - "limit_disclosure": "required", - "fields": [ - { - "path": [ - "$.mdoc.doctype" - ], - "filter": { - "type": "string", - "const": "org.iso.18013.5.1.mDL" - } - }, - { - "path": [ - "$.mdoc.namespace" - ], - "filter": { - "type": "string", - "const": "org.iso.18013.5.1" - } - }, - { - "path": [ - "$.mdoc.family_name" - ], - "intent_to_retain": "false" - }, - { - "path": [ - "$.mdoc.portrait" - ], - "intent_to_retain": "false" - }, - { - "path": [ - "$.mdoc.driving_privileges" - ], - "intent_to_retain": "false" - } - ] - } - } - } - ] - } - ], "default_max_age": 1111, diff --git a/pyeudiw/tests/federation/test_schema.py b/pyeudiw/tests/federation/test_schema.py index 7c5d2d2f..37559f76 100644 --- a/pyeudiw/tests/federation/test_schema.py +++ b/pyeudiw/tests/federation/test_schema.py @@ -41,7 +41,40 @@ 'request_uris': [], 'redirect_uris': [], 'default_acr_values': [], - 'presentation_definitions': [], + 'presentation_definition': { + "id": "d76c51b7-ea90-49bb-8368-6b3d194fc131", + "input_descriptors": [ + { + "id": "IdentityCredential", + "format": { + "vc+sd-jwt": {} + }, + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": [ + "$.vct" + ], + "filter": { + "type": "string", + "const": "IdentityCredential" + } + }, + { + "path": [ + "$.family_name" + ] + }, + { + "path": [ + "$.given_name" + ] + } + ] + } + } + ]}, 'authorization_signed_response_alg': ['RS256'], 'authorization_encrypted_response_alg': ["RSA-OAEP"], 'authorization_encrypted_response_enc': ["A128CBC-HS256"], diff --git a/pyeudiw/tests/openid4vp/schemas/test_schema.py b/pyeudiw/tests/openid4vp/schemas/test_schema.py index 6496811e..6f63e067 100644 --- a/pyeudiw/tests/openid4vp/schemas/test_schema.py +++ b/pyeudiw/tests/openid4vp/schemas/test_schema.py @@ -152,121 +152,41 @@ def test_entity_config_payload(): ] } }, - "presentation_definitions": [ - { - "id": "pid-sd-jwt:unique_id+given_name+family_name", - "input_descriptors": [ - { - "id": "sd-jwt", - "format": { - "jwt": { - "alg": [ - "EdDSA", - "ES256" - ] + "presentation_definition": { + "id": "d76c51b7-ea90-49bb-8368-6b3d194fc131", + "input_descriptors": [ + { + "id": "IdentityCredential", + "format": { + "vc+sd-jwt": {} + }, + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": [ + "$.vct" + ], + "filter": { + "type": "string", + "const": "IdentityCredential" + } }, - "constraints": { - "limit_disclosure": "required", - "fields": [ - { - "path": [ - "$.sd-jwt.type" - ], - "filter": { - "type": "string", - "const": "PersonIdentificationData" - } - }, - { - "path": [ - "$.sd-jwt.cnf" - ], - "filter": { - "type": "object" - } - }, - { - "path": [ - "$.sd-jwt.family_name" - ], - "intent_to_retain": "true" - }, - { - "path": [ - "$.sd-jwt.given_name" - ], - "intent_to_retain": "true" - }, - { - "path": [ - "$.sd-jwt.unique_id" - ], - "intent_to_retain": "true" - } - ] - } - } - } - ] - }, - { - "id": "mDL-sample-req", - "input_descriptors": [ - { - "id": "mDL", - "format": { - "mso_mdoc": { - "alg": [ - "EdDSA", - "ES256" + { + "path": [ + "$.family_name" ] }, - "constraints": { - "limit_disclosure": "required", - "fields": [ - { - "path": [ - "$.mdoc.doctype" - ], - "filter": { - "type": "string", - "const": "org.iso.18013.5.1.mDL" - } - }, - { - "path": [ - "$.mdoc.namespace" - ], - "filter": { - "type": "string", - "const": "org.iso.18013.5.1" - } - }, - { - "path": [ - "$.mdoc.family_name" - ], - "intent_to_retain": "false" - }, - { - "path": [ - "$.mdoc.portrait" - ], - "intent_to_retain": "false" - }, - { - "path": [ - "$.mdoc.driving_privileges" - ], - "intent_to_retain": "false" - } + { + "path": [ + "$.given_name" ] } - } + ] } - ] - } - ], + } + ] + }, "default_max_age": 1111, "authorization_signed_response_alg": [ "RS256", From b2800923f4aa7cba87e1286362bbdfb69430e063 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Mon, 18 Dec 2023 12:54:05 +0100 Subject: [PATCH 18/19] fix(commit): validate the schema after init Move the validation at the end of the initialization since some fields are transformed by the `__init__` function rather than simply loaded. Update the above commit with respect to the new modifications. --- pyeudiw/satosa/backend.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pyeudiw/satosa/backend.py b/pyeudiw/satosa/backend.py index 2cd3f245..f7b57a47 100644 --- a/pyeudiw/satosa/backend.py +++ b/pyeudiw/satosa/backend.py @@ -74,12 +74,6 @@ def __init__( """ super().__init__(auth_callback_func, internal_attributes, base_url, name) - try: - WalletRelyingParty(**config['metadata']) - except ValidationError as e: - debug_message = f"""The backend configuration presents the following validation issues: {e}""" - self._log_warning("OpenID4VPBackend", debug_message) - self.config = config self.client_id = self.config['metadata']['client_id'] self.default_exp = int(self.config['jwt']['default_exp']) @@ -104,6 +98,11 @@ def __init__( # resolve metadata pointers/placeholders self._render_metadata_conf_elements() self.init_trust_resources() + try: + WalletRelyingParty(**config['metadata']) + except ValidationError as e: + debug_message = f"""The backend configuration presents the following validation issues: {e}""" + self._log_warning("OpenID4VPBackend", debug_message) self._log_debug("OpenID4VP init", f"Loaded configuration: {json.dumps(config)}") def register_endpoints(self) -> list[tuple[str, Callable[[Context], Response]]]: From 726dd90e8779d030a5067b318be6989db2d1cf8a Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Mon, 18 Dec 2023 13:55:00 +0100 Subject: [PATCH 19/19] test: update tests to use EC keys Migrate tests to EC keys. Remove duplicated code. --- example/satosa/integration_test/main.py | 4 +- example/satosa/integration_test/settings.py | 6 +- .../satosa/integration_test/settings_ec.py | 135 ----------- pyeudiw/tests/federation/base.py | 29 +-- pyeudiw/tests/federation/base_ec.py | 218 ------------------ .../test_static_trust_chain_validator.py | 6 +- pyeudiw/tests/satosa/test_backend.py | 8 +- 7 files changed, 28 insertions(+), 378 deletions(-) delete mode 100644 example/satosa/integration_test/settings_ec.py delete mode 100644 pyeudiw/tests/federation/base_ec.py diff --git a/example/satosa/integration_test/main.py b/example/satosa/integration_test/main.py index f99af9dd..49aabe4c 100644 --- a/example/satosa/integration_test/main.py +++ b/example/satosa/integration_test/main.py @@ -7,7 +7,7 @@ from pyeudiw.jwt import DEFAULT_SIG_KTY_MAP from pyeudiw.presentation_exchange.schemas.oid4vc_presentation_definition import PresentationDefinition -from pyeudiw.tests.federation.base_ec import ( +from pyeudiw.tests.federation.base import ( EXP, leaf_cred, leaf_cred_jwk, @@ -37,7 +37,7 @@ from saml2_sp import saml2_request, IDP_BASEURL from sd_jwt.holder import SDJWTHolder -from settings_ec import ( +from settings import ( CONFIG_DB, RP_EID, WALLET_INSTANCE_ATTESTATION, diff --git a/example/satosa/integration_test/settings.py b/example/satosa/integration_test/settings.py index ea20cf4c..ae3ffe9c 100644 --- a/example/satosa/integration_test/settings.py +++ b/example/satosa/integration_test/settings.py @@ -110,7 +110,7 @@ ] } rp_signer = JWS( - rp_ec, alg="RS256", + rp_ec, alg="ES256", typ="application/entity-statement+jwt" ) @@ -125,11 +125,11 @@ } } ta_signer = JWS( - _es, alg="RS256", + _es, alg="ES256", typ="application/entity-statement+jwt" ) its_trust_chain = [ - rp_signer.sign_compact([key_from_jwk_dict(rp_jwks[0])]), + rp_signer.sign_compact([key_from_jwk_dict(rp_jwks[1])]), ta_signer.sign_compact([ta_jwk]) ] diff --git a/example/satosa/integration_test/settings_ec.py b/example/satosa/integration_test/settings_ec.py deleted file mode 100644 index 87b58886..00000000 --- a/example/satosa/integration_test/settings_ec.py +++ /dev/null @@ -1,135 +0,0 @@ - -from cryptojwt.jws.jws import JWS -from cryptojwt.jwk.jwk import key_from_jwk_dict -from pyeudiw.tests.federation.base_ec import ( - NOW, - EXP, - leaf_wallet_jwk, - ta_ec, - ta_jwk -) - -from pyeudiw.tools.utils import iat_now, exp_from_now - - -RP_EID = "https://localhost/OpenID4VP" - -CONFIG_DB = { - "mongo_db": { - "storage": { - "module": "pyeudiw.storage.mongo_storage", - "class": "MongoStorage", - "init_params": { - "url": "mongodb://localhost:27017/", - "conf": { - "db_name": "eudiw", - "db_sessions_collection": "sessions", - "db_trust_attestations_collection": "trust_attestations", - "db_trust_anchors_collection": "trust_anchors" - }, - "connection_params": {} - } - } - } -} - - -WALLET_INSTANCE_ATTESTATION = { - "iss": "https://wallet-provider.example.org", - "sub": "vbeXJksM45xphtANnCiG6mCyuU4jfGNzopGuKvogg9c", - "type": "WalletInstanceAttestation", - "policy_uri": "https://wallet-provider.example.org/privacy_policy", - "tos_uri": "https://wallet-provider.example.org/info_policy", - "logo_uri": "https://wallet-provider.example.org/logo.svg", - "asc": "https://wallet-provider.example.org/LoA/basic", - "cnf": - { - "jwk": leaf_wallet_jwk.serialize() - }, - "authorization_endpoint": "eudiw:", - "response_types_supported": [ - "vp_token" - ], - "vp_formats_supported": { - "jwt_vp_json": { - "alg_values_supported": ["ES256"] - }, - "jwt_vc_json": { - "alg_values_supported": ["ES256"] - } - }, - "request_object_signing_alg_values_supported": [ - "ES256" - ], - "presentation_definition_uri_supported": False, - "iat": iat_now(), - "exp": exp_from_now() -} - -rp_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" - }, - { - 'kty': 'EC', - 'kid': 'xPFTWxeGHTVTaDlzGad0MKN5JmWOSnRqEjJCtvQpoyg', - 'crv': 'P-256', - 'x': 'EkMoe7qPLGMydWO_evC3AXEeXJlLQk9tNRkYcpp7xHo', - 'y': 'VLoHFl90D1SdTTjMvNf3WssWiCBXcU1lGNPbOmcCqdU', - 'd': 'oGzjgBbIYNL9opdJ_rDPnCJF89yN8yj8wegdkYfaxw0' - } -] -rp_ec = { - "exp": EXP, - "iat": NOW, - "iss": RP_EID, - "sub": RP_EID, - 'jwks': {"keys": rp_jwks}, - "metadata": { - "wallet_relying_party": { - 'jwks': {"keys": []} - }, - "federation_entity": { - "organization_name": "OpenID Wallet Verifier example", - "homepage_uri": "https://verifier.example.org/home", - "policy_uri": "https://verifier.example.org/policy", - "logo_uri": "https://verifier.example.org/static/logo.svg", - "contacts": [ - "tech@verifier.example.org" - ] - } - }, - "authority_hints": [ - ta_ec['iss'] - ] -} -rp_signer = JWS( - rp_ec, alg="ES256", - typ="application/entity-statement+jwt" -) - - -_es = ta_es = { - "exp": EXP, - "iat": NOW, - "iss": ta_ec['iss'], - "sub": RP_EID, - 'jwks': { - 'keys': rp_jwks - } -} -ta_signer = JWS( - _es, alg="ES256", - typ="application/entity-statement+jwt" -) - -its_trust_chain = [ - rp_signer.sign_compact([key_from_jwk_dict(rp_jwks[1])]), - ta_signer.sign_compact([ta_jwk]) -] diff --git a/pyeudiw/tests/federation/base.py b/pyeudiw/tests/federation/base.py index 63b34ef9..625e1d9d 100644 --- a/pyeudiw/tests/federation/base.py +++ b/pyeudiw/tests/federation/base.py @@ -1,5 +1,5 @@ +from cryptojwt.jwk.ec import new_ec_key from cryptojwt.jws.jws import JWS -from cryptojwt.jwk.rsa import new_rsa_key import json import pyeudiw.federation.trust_chain_validator as tcv_test @@ -13,15 +13,18 @@ NOW = iat_now() EXP = exp_from_now(5000) +ec_crv = "P-256" +ec_alg = "ES256" + # Define intermediate ec -intermediate_jwk = new_rsa_key() +intermediate_jwk = new_ec_key(ec_crv, alg=ec_alg) # Define TA ec -ta_jwk = new_rsa_key() +ta_jwk = new_ec_key(ec_crv, alg=ec_alg) # Define leaf Credential Issuer -leaf_cred_jwk = new_rsa_key() -leaf_cred_jwk_prot = new_rsa_key() +leaf_cred_jwk = new_ec_key(ec_crv, alg=ec_alg) +leaf_cred_jwk_prot = new_ec_key(ec_crv, alg=ec_alg) leaf_cred = { "exp": EXP, "iat": NOW, @@ -62,7 +65,7 @@ intermediate_es_cred["jwks"]['keys'] = [leaf_cred_jwk.serialize()] # Define leaf Wallet Provider -leaf_wallet_jwk = new_rsa_key() +leaf_wallet_jwk = new_ec_key(ec_crv, alg=ec_alg) leaf_wallet = { "exp": EXP, "iat": NOW, @@ -155,17 +158,17 @@ } # Sign step -leaf_cred_signer = JWS(leaf_cred, alg='RS256', +leaf_cred_signer = JWS(leaf_cred, alg=ec_alg, typ='entity-statement+jwt') leaf_cred_signed = leaf_cred_signer.sign_compact([leaf_cred_jwk]) -leaf_wallet_signer = JWS(leaf_wallet, alg='RS256', +leaf_wallet_signer = JWS(leaf_wallet, alg=ec_alg, typ='entity-statement+jwt') leaf_wallet_signed = leaf_wallet_signer.sign_compact([leaf_wallet_jwk]) intermediate_signer_ec = JWS( - intermediate_ec, alg="RS256", + intermediate_ec, alg=ec_alg, typ="entity-statement+jwt" ) intermediate_ec_signed = intermediate_signer_ec.sign_compact([ @@ -173,19 +176,19 @@ intermediate_signer_es_cred = JWS( - intermediate_es_cred, alg='RS256', typ='entity-statement+jwt') + intermediate_es_cred, alg=ec_alg, typ='entity-statement+jwt') intermediate_es_cred_signed = intermediate_signer_es_cred.sign_compact([ intermediate_jwk]) intermediate_signer_es_wallet = JWS( - intermediate_es_wallet, alg='RS256', typ='entity-statement+jwt') + intermediate_es_wallet, alg=ec_alg, typ='entity-statement+jwt') intermediate_es_wallet_signed = intermediate_signer_es_wallet.sign_compact([ intermediate_jwk]) -ta_es_signer = JWS(ta_es, alg="RS256", typ="entity-statement+jwt") +ta_es_signer = JWS(ta_es, alg=ec_alg, typ="entity-statement+jwt") ta_es_signed = ta_es_signer.sign_compact([ta_jwk]) -ta_ec_signer = JWS(ta_ec, alg="RS256", typ="entity-statement+jwt") +ta_ec_signer = JWS(ta_ec, alg=ec_alg, typ="entity-statement+jwt") ta_ec_signed = ta_ec_signer.sign_compact([ta_jwk]) diff --git a/pyeudiw/tests/federation/base_ec.py b/pyeudiw/tests/federation/base_ec.py deleted file mode 100644 index 625e1d9d..00000000 --- a/pyeudiw/tests/federation/base_ec.py +++ /dev/null @@ -1,218 +0,0 @@ -from cryptojwt.jwk.ec import new_ec_key -from cryptojwt.jws.jws import JWS - -import json -import pyeudiw.federation.trust_chain_validator as tcv_test -from pyeudiw.tools.utils import iat_now, exp_from_now - -httpc_params = { - "connection": {"ssl": True}, - "session": {"timeout": 6}, -} - -NOW = iat_now() -EXP = exp_from_now(5000) - -ec_crv = "P-256" -ec_alg = "ES256" - -# Define intermediate ec -intermediate_jwk = new_ec_key(ec_crv, alg=ec_alg) - -# Define TA ec -ta_jwk = new_ec_key(ec_crv, alg=ec_alg) - -# Define leaf Credential Issuer -leaf_cred_jwk = new_ec_key(ec_crv, alg=ec_alg) -leaf_cred_jwk_prot = new_ec_key(ec_crv, alg=ec_alg) -leaf_cred = { - "exp": EXP, - "iat": NOW, - "iss": "https://credential_issuer.example.org", - "sub": "https://credential_issuer.example.org", - 'jwks': {"keys": []}, - "metadata": { - "openid_credential_issuer": { - 'jwks': {"keys": []} - }, - "federation_entity": { - "organization_name": "OpenID Credential Issuer example", - "homepage_uri": "https://credential_issuer.example.org/home", - "policy_uri": "https://credential_issuer.example.org/policy", - "logo_uri": "https://credential_issuer.example.org/static/logo.svg", - "contacts": [ - "tech@credential_issuer.example.org" - ] - } - }, - "authority_hints": [ - "https://intermediate.eidas.example.org" - ] -} -leaf_cred['jwks']['keys'] = [leaf_cred_jwk.serialize()] -leaf_cred['metadata']['openid_credential_issuer']['jwks']['keys'] = [ - leaf_cred_jwk_prot.serialize()] - - -# Define intermediate Entity Statement for credential -intermediate_es_cred = { - "exp": EXP, - "iat": NOW, - "iss": "https://intermediate.eidas.example.org", - "sub": "https://credential_issuer.example.org", - 'jwks': {"keys": []} -} -intermediate_es_cred["jwks"]['keys'] = [leaf_cred_jwk.serialize()] - -# Define leaf Wallet Provider -leaf_wallet_jwk = new_ec_key(ec_crv, alg=ec_alg) -leaf_wallet = { - "exp": EXP, - "iat": NOW, - "iss": "https://wallet-provider.example.org", - "sub": "https://wallet-provider.example.org", - 'jwks': {"keys": []}, - "metadata": { - "wallet_provider": { - "jwks": {"keys": []} - }, - "federation_entity": { - "organization_name": "OpenID Wallet Verifier example", - "homepage_uri": "https://wallet-provider.example.org/home", - "policy_uri": "https://wallet-provider.example.org/policy", - "logo_uri": "https://wallet-provider.example.org/static/logo.svg", - "contacts": [ - "tech@wallet-provider.example.org" - ] - } - }, - "authority_hints": [ - "https://intermediate.eidas.example.org" - ] -} -leaf_wallet['jwks']['keys'] = [leaf_wallet_jwk.serialize()] -leaf_wallet['metadata']['wallet_provider'] = [leaf_wallet_jwk.serialize()] - -# Define intermediate Entity Statement for wallet provider -intermediate_es_wallet = { - "exp": EXP, - "iat": NOW, - "iss": "https://intermediate.eidas.example.org", - "sub": "https://wallet-provider.example.org", - 'jwks': {"keys": [leaf_wallet_jwk.serialize()]} -} - -# Intermediate EC -intermediate_ec = { - "exp": EXP, - "iat": NOW, - 'iss': 'https://intermediate.eidas.example.org', - 'sub': 'https://intermediate.eidas.example.org', - 'jwks': {"keys": [intermediate_jwk.serialize()]}, - 'metadata': { - 'federation_entity': { - 'contacts': ['soggetto@intermediate.eidas.example.it'], - 'federation_fetch_endpoint': 'https://intermediate.eidas.example.org/fetch', - 'federation_resolve_endpoint': 'https://intermediate.eidas.example.org/resolve', - 'federation_list_endpoint': 'https://intermediate.eidas.example.org/list', - 'homepage_uri': 'https://soggetto.intermediate.eidas.example.it', - 'name': 'Example Intermediate intermediate.eidas.example' - } - }, - "authority_hints": [ - "https://trust-anchor.example.org" - ] -} - - -# Define TA -ta_es = { - "exp": EXP, - "iat": NOW, - "iss": "https://trust-anchor.example.org", - "sub": "https://intermediate.eidas.example.org", - 'jwks': {"keys": [intermediate_jwk.serialize()]} -} - -ta_ec = { - "exp": EXP, - "iat": NOW, - "iss": "https://trust-anchor.example.org", - "sub": "https://trust-anchor.example.org", - 'jwks': {"keys": [ta_jwk.serialize()]}, - "metadata": { - "federation_entity": { - 'federation_fetch_endpoint': 'https://trust-anchor.example.org/fetch', - 'federation_resolve_endpoint': 'https://trust-anchor.example.org/resolve', - 'federation_list_endpoint': 'https://trust-anchor.example.org/list', - "organization_name": "TA example", - "homepage_uri": "https://trust-anchor.example.org/home", - "policy_uri": "https://trust-anchor.example.org/policy", - "logo_uri": "https://trust-anchor.example.org/static/logo.svg", - "contacts": [ - "tech@trust-anchor.example.org" - ] - } - }, - 'constraints': {'max_path_length': 1} -} - -# Sign step -leaf_cred_signer = JWS(leaf_cred, alg=ec_alg, - typ='entity-statement+jwt') -leaf_cred_signed = leaf_cred_signer.sign_compact([leaf_cred_jwk]) - -leaf_wallet_signer = JWS(leaf_wallet, alg=ec_alg, - typ='entity-statement+jwt') -leaf_wallet_signed = leaf_wallet_signer.sign_compact([leaf_wallet_jwk]) - - -intermediate_signer_ec = JWS( - intermediate_ec, alg=ec_alg, - typ="entity-statement+jwt" -) -intermediate_ec_signed = intermediate_signer_ec.sign_compact([ - intermediate_jwk]) - - -intermediate_signer_es_cred = JWS( - intermediate_es_cred, alg=ec_alg, typ='entity-statement+jwt') -intermediate_es_cred_signed = intermediate_signer_es_cred.sign_compact([ - intermediate_jwk]) - -intermediate_signer_es_wallet = JWS( - intermediate_es_wallet, alg=ec_alg, typ='entity-statement+jwt') -intermediate_es_wallet_signed = intermediate_signer_es_wallet.sign_compact([ - intermediate_jwk]) - -ta_es_signer = JWS(ta_es, alg=ec_alg, typ="entity-statement+jwt") -ta_es_signed = ta_es_signer.sign_compact([ta_jwk]) - -ta_ec_signer = JWS(ta_ec, alg=ec_alg, typ="entity-statement+jwt") -ta_ec_signed = ta_ec_signer.sign_compact([ta_jwk]) - - -trust_chain_issuer = [ - leaf_cred_signed, - intermediate_es_cred_signed, - ta_es_signed, - ta_ec_signed -] - -trust_chain_wallet = [ - leaf_wallet_signed, - intermediate_es_wallet_signed, - ta_es_signed -] - -test_cred = tcv_test.StaticTrustChainValidator( - trust_chain_issuer, [ta_jwk.serialize()], httpc_params=httpc_params -) -assert test_cred.is_valid - -test_wallet = tcv_test.StaticTrustChainValidator( - trust_chain_wallet, [ta_jwk.serialize()], httpc_params=httpc_params -) -assert test_wallet.is_valid - -print(json.dumps(trust_chain_issuer, indent=2)) diff --git a/pyeudiw/tests/federation/test_static_trust_chain_validator.py b/pyeudiw/tests/federation/test_static_trust_chain_validator.py index 15b079e3..1f48df13 100644 --- a/pyeudiw/tests/federation/test_static_trust_chain_validator.py +++ b/pyeudiw/tests/federation/test_static_trust_chain_validator.py @@ -37,7 +37,7 @@ def test_is_valid(): invalid_intermediate["jwks"]['keys'] = [invalid_leaf_jwk] intermediate_signer = JWS( - invalid_intermediate, alg="RS256", + invalid_intermediate, alg="ES256", typ="application/entity-statement+jwt" ) invalid_intermediate_es_wallet_signed = intermediate_signer.sign_compact( @@ -110,7 +110,7 @@ def test_update_st_es_case_source_endpoint(): "source_endpoint": "https://trust-anchor.example.org/fetch" } - ta_signer = JWS(ta_es, alg="RS256", typ="application/entity-statement+jwt") + ta_signer = JWS(ta_es, alg="ES256", typ="application/entity-statement+jwt") ta_es_signed = ta_signer.sign_compact([ta_jwk]) def mock_method(*args, **kwargs): @@ -133,7 +133,7 @@ def test_update_st_es_case_no_source_endpoint(): 'jwks': {"keys": []}, } - ta_signer = JWS(ta_es, alg="RS256", typ="application/entity-statement+jwt") + ta_signer = JWS(ta_es, alg="ES256", typ="application/entity-statement+jwt") ta_es_signed = ta_signer.sign_compact([ta_jwk]) def mock_method_ec(*args, **kwargs): diff --git a/pyeudiw/tests/satosa/test_backend.py b/pyeudiw/tests/satosa/test_backend.py index bd8eb714..9f64f962 100644 --- a/pyeudiw/tests/satosa/test_backend.py +++ b/pyeudiw/tests/satosa/test_backend.py @@ -22,7 +22,7 @@ _adapt_keys, issue_sd_jwt, load_specification_from_yaml_string, - import_pyca_pri_rsa + import_ec ) from pyeudiw.storage.db_engine import DBEngine from pyeudiw.tools.utils import exp_from_now, iat_now @@ -188,7 +188,7 @@ def test_vp_validation_in_redirect_endpoint(self, context): {}, nonce, str(uuid.uuid4()), - import_pyca_pri_rsa(holder_jwk.key.priv_key, kid=holder_jwk.kid) if sd_specification.get( + import_ec(holder_jwk.key.priv_key, kid=holder_jwk.kid) if sd_specification.get( "key_binding", False) else None, sign_alg=DEFAULT_SIG_KTY_MAP[holder_jwk.key.kty], ) @@ -341,7 +341,7 @@ def test_redirect_endpoint(self, context): {}, nonce, str(uuid.uuid4()), - import_pyca_pri_rsa(holder_jwk.key.priv_key, kid=holder_jwk.kid) if sd_specification.get( + import_ec(holder_jwk.key.priv_key, kid=holder_jwk.kid) if sd_specification.get( "key_binding", False) else None, sign_alg=DEFAULT_SIG_KTY_MAP[holder_jwk.key.kty], ) @@ -485,7 +485,7 @@ def test_request_endpoint(self, context): "sub": self.backend.client_id, 'jwks': self.backend.entity_configuration_as_dict['jwks'] } - ta_signer = JWS(_es, alg="RS256", + ta_signer = JWS(_es, alg="ES256", typ="application/entity-statement+jwt") its_trust_chain = [