From c56bc43b74f3f2ca5d3645369f88e326dfcbc691 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso <32564922+salvatorelaiso@users.noreply.github.com> Date: Wed, 20 Dec 2023 00:45:53 +0100 Subject: [PATCH] Elliptic curve support (#200) * fix: define a custom error for unsupported key while encrypting * fix: JWE encryption with EC key * fix: JWE decryption with EC key * feat: adapt EC keys * test: integration test with EC keys * feat: dynamic JWK schema loading * 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 * fix: imports * chore: keep only EC keys based integration test * fix: elliptic curve name support * fix: remove redundant schema * fix: update integration test to handle uppercase chars * fix: presentation definition validation in integration test * fix: remove port 10000 * 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. * fix: extend valid authorization algs * fix: update presentation definition in examples * 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. * test: update tests to use EC keys Migrate tests to EC keys. Remove duplicated code. --------- Co-authored-by: Salvatore Laiso --- example/satosa/integration_test/main.py | 25 ++- .../satosa/integration_test/metadata/idp.xml | 2 +- example/satosa/integration_test/saml2_sp.py | 2 +- example/satosa/integration_test/settings.py | 8 +- example/satosa/pyeudiw_backend.yaml | 75 ++------- example/satosa/static/disco.html | 2 +- .../schemas/wallet_relying_party.py | 10 +- pyeudiw/jwk/schemas/__init__.py | 0 pyeudiw/jwk/schemas/jwk.py | 28 ++++ pyeudiw/jwt/__init__.py | 19 ++- pyeudiw/jwt/exceptions.py | 5 +- pyeudiw/oauth2/dpop/schema.py | 5 +- .../schemas/oid4vc_presentation_definition.py | 4 - .../schemas/presentation_definition.py | 29 ---- pyeudiw/satosa/backend.py | 11 +- pyeudiw/sd_jwt/__init__.py | 33 +++- pyeudiw/sd_jwt/exceptions.py | 2 + pyeudiw/tests/federation/base.py | 29 ++-- .../schemas/test_entity_configuration.py | 134 +++------------ pyeudiw/tests/federation/test_schema.py | 35 +++- .../test_static_trust_chain_validator.py | 6 +- .../tests/openid4vp/schemas/test_schema.py | 138 ++++------------ .../schemas/test_presentation_definition.py | 155 +----------------- ...definition_for_a_high_assurance_profile.py | 13 -- pyeudiw/tests/satosa/test_backend.py | 8 +- pyeudiw/tests/test_jwk.py | 17 ++ pyeudiw/tests/test_jwt.py | 18 +- 27 files changed, 281 insertions(+), 532 deletions(-) create mode 100644 pyeudiw/jwk/schemas/__init__.py create mode 100644 pyeudiw/jwk/schemas/jwk.py delete mode 100644 pyeudiw/presentation_exchange/schemas/presentation_definition.py create mode 100644 pyeudiw/sd_jwt/exceptions.py delete mode 100644 pyeudiw/tests/presentation_exchange/schemas/test_presentation_definition_for_a_high_assurance_profile.py diff --git a/example/satosa/integration_test/main.py b/example/satosa/integration_test/main.py index a4459d4d..49aabe4c 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 import ( EXP, leaf_cred, @@ -27,10 +28,10 @@ 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.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 +128,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 @@ -191,7 +192,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 ) @@ -200,7 +201,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 +224,10 @@ 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) + +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] @@ -264,9 +268,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 +287,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() 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_ITMIIGJjCCBI6gAwIBAgIUfU0kpXVz4VKab7plowh6WarIYywwDQYJKoZIhvcNAQELBQAwgYoxJDAiBgNVBAoMG0EgQ29tcGFueSBNYWtpbmcgRXZlcnl0aGluZzEQMA4GA1UEAwwHQS5DLk0uRTEdMBsGA1UEUwwUaHR0cHM6Ly9zcGlkLmFjbWUuaXQxFTATBgNVBGEMDFBBOklULWNfaDUwMTELMAkGA1UEBhMCSVQxDTALBgNVBAcMBFJvbWEwHhcNMjIxMTE5MTY1MjIwWhcNMzIxMTE2MTY1MjIwWjCBijEkMCIGA1UECgwbQSBDb21wYW55IE1ha2luZyBFdmVyeXRoaW5nMRAwDgYDVQQDDAdBLkMuTS5FMR0wGwYDVQRTDBRodHRwczovL3NwaWQuYWNtZS5pdDEVMBMGA1UEYQwMUEE6SVQtY19oNTAxMQswCQYDVQQGEwJJVDENMAsGA1UEBwwEUm9tYTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAJJL67gVrM6SNxiulqto4f8v1SqJwmdaR9/TTubScNzI6d2JnirqQ6a6urBiqP3KfRUrbGUMZ65Uw9T6fEDBSmizy9AkwQvhVie8KbbIA7xxTLr9zPq5LMQA1zKYAkUgEMvyPf6bJCMVEQBMoOt4qok+JDRcpznw5MP3lNCuvYxtqzBf3m7o+YMKhxSUbVaMr2gGLjW2hWYKd663iJ1ZzHvWKCL8KkEzCLwLfoCgHbiPHobVghTqePuqUe35gYq9MhmELBj5GArlWFp38fRP6DGudGye+qF3/4z1Bzj9TDt2sMaCdt00WCoq99OLRGFR2m7v81Z2o/3hDJncgIBj+vpj3EwUMc6JrCY3liMJcyjkHT940dbUF5LEMD0frePgn9/vE2pTjN5CRU5794q9XavOL9peORMxYsrI5qQyqUo39qA7pixqs9csUCsdnmBFLe7xk/qLMe5f5NREvzryS7WR1cVO81ZoTc7tD7bZChLjnJiZQBDzjIuSJwtlN164QQIDAQABo4IBgDCCAXwwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBsAwcwYDVR0gBGwwajAfBgMrTBAwGDAWBggrBgEFBQcCAjAKDAhBZ0lEcm9vdDAgBgQrTBAGMBgwFgYIKwYBBQUHAgIwCgwIYWdJRGNlcnQwJQYGK0wQBAIBMBswGQYIKwYBBQUHAgIwDQwLY2VydF9TUF9QdWIwHQYDVR0OBBYEFNFS033ubTPFuD5Elo92XroK3yvpMIHKBgNVHSMEgcIwgb+AFNFS033ubTPFuD5Elo92XroK3yvpoYGQpIGNMIGKMSQwIgYDVQQKDBtBIENvbXBhbnkgTWFraW5nIEV2ZXJ5dGhpbmcxEDAOBgNVBAMMB0EuQy5NLkUxHTAbBgNVBFMMFGh0dHBzOi8vc3BpZC5hY21lLml0MRUwEwYDVQRhDAxQQTpJVC1jX2g1MDExCzAJBgNVBAYTAklUMQ0wCwYDVQQHDARSb21hghR9TSSldXPhUppvumWjCHpZqshjLDANBgkqhkiG9w0BAQsFAAOCAYEAZsT4xgbQo6lStFg1+7u+USWjil4FZIbadEl6qL4FjmWa+uGgFRO0Z2wvTl4Ek+WE94SqgQNwaZmebGAc9pxb7M5vH9NnxVgN0MHt758aVBX967wVoVM5lFGHx7d6jMYW9LiyYxcxD40zbZW0tFB8YuTPImjL1GiM2npY7jCRb/ZAxz0QcpTvZG98eR/WJprR8siniKkxC+PFYxzhsOntp+7r5UHrvN0WMjJEehjVNaUcowLDsTMIQGO0VIUF3jTOPikUtpRR4V5MluDS0dysmEYyOgUvt1hYC5LkoJ2v1tBH7AxzwkFpVtvTVNtFdotO1ZDMAlpDHA3d0LuGiM4JMfH87DkTCh+Mb4RNaeBfXDo+YCG7ueslLmjCzcjKjAr2QGdhfLnEdx/Ozn8CnMLOj+2PQ4rrfZ2ijzvv7dUNnbOs36DTrxbNys0BEQu0MhAoMzX6xAecDzd+FNnc+/+TK/xQ2pDZjxTYdwitJF0szdErUt11NzK85QNBL0JjCDWHMIIGJjCCBI6gAwIBAgIUfU0kpXVz4VKab7plowh6WarIYywwDQYJKoZIhvcNAQELBQAwgYoxJDAiBgNVBAoMG0EgQ29tcGFueSBNYWtpbmcgRXZlcnl0aGluZzEQMA4GA1UEAwwHQS5DLk0uRTEdMBsGA1UEUwwUaHR0cHM6Ly9zcGlkLmFjbWUuaXQxFTATBgNVBGEMDFBBOklULWNfaDUwMTELMAkGA1UEBhMCSVQxDTALBgNVBAcMBFJvbWEwHhcNMjIxMTE5MTY1MjIwWhcNMzIxMTE2MTY1MjIwWjCBijEkMCIGA1UECgwbQSBDb21wYW55IE1ha2luZyBFdmVyeXRoaW5nMRAwDgYDVQQDDAdBLkMuTS5FMR0wGwYDVQRTDBRodHRwczovL3NwaWQuYWNtZS5pdDEVMBMGA1UEYQwMUEE6SVQtY19oNTAxMQswCQYDVQQGEwJJVDENMAsGA1UEBwwEUm9tYTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAJJL67gVrM6SNxiulqto4f8v1SqJwmdaR9/TTubScNzI6d2JnirqQ6a6urBiqP3KfRUrbGUMZ65Uw9T6fEDBSmizy9AkwQvhVie8KbbIA7xxTLr9zPq5LMQA1zKYAkUgEMvyPf6bJCMVEQBMoOt4qok+JDRcpznw5MP3lNCuvYxtqzBf3m7o+YMKhxSUbVaMr2gGLjW2hWYKd663iJ1ZzHvWKCL8KkEzCLwLfoCgHbiPHobVghTqePuqUe35gYq9MhmELBj5GArlWFp38fRP6DGudGye+qF3/4z1Bzj9TDt2sMaCdt00WCoq99OLRGFR2m7v81Z2o/3hDJncgIBj+vpj3EwUMc6JrCY3liMJcyjkHT940dbUF5LEMD0frePgn9/vE2pTjN5CRU5794q9XavOL9peORMxYsrI5qQyqUo39qA7pixqs9csUCsdnmBFLe7xk/qLMe5f5NREvzryS7WR1cVO81ZoTc7tD7bZChLjnJiZQBDzjIuSJwtlN164QQIDAQABo4IBgDCCAXwwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBsAwcwYDVR0gBGwwajAfBgMrTBAwGDAWBggrBgEFBQcCAjAKDAhBZ0lEcm9vdDAgBgQrTBAGMBgwFgYIKwYBBQUHAgIwCgwIYWdJRGNlcnQwJQYGK0wQBAIBMBswGQYIKwYBBQUHAgIwDQwLY2VydF9TUF9QdWIwHQYDVR0OBBYEFNFS033ubTPFuD5Elo92XroK3yvpMIHKBgNVHSMEgcIwgb+AFNFS033ubTPFuD5Elo92XroK3yvpoYGQpIGNMIGKMSQwIgYDVQQKDBtBIENvbXBhbnkgTWFraW5nIEV2ZXJ5dGhpbmcxEDAOBgNVBAMMB0EuQy5NLkUxHTAbBgNVBFMMFGh0dHBzOi8vc3BpZC5hY21lLml0MRUwEwYDVQRhDAxQQTpJVC1jX2g1MDExCzAJBgNVBAYTAklUMQ0wCwYDVQQHDARSb21hghR9TSSldXPhUppvumWjCHpZqshjLDANBgkqhkiG9w0BAQsFAAOCAYEAZsT4xgbQo6lStFg1+7u+USWjil4FZIbadEl6qL4FjmWa+uGgFRO0Z2wvTl4Ek+WE94SqgQNwaZmebGAc9pxb7M5vH9NnxVgN0MHt758aVBX967wVoVM5lFGHx7d6jMYW9LiyYxcxD40zbZW0tFB8YuTPImjL1GiM2npY7jCRb/ZAxz0QcpTvZG98eR/WJprR8siniKkxC+PFYxzhsOntp+7r5UHrvN0WMjJEehjVNaUcowLDsTMIQGO0VIUF3jTOPikUtpRR4V5MluDS0dysmEYyOgUvt1hYC5LkoJ2v1tBH7AxzwkFpVtvTVNtFdotO1ZDMAlpDHA3d0LuGiM4JMfH87DkTCh+Mb4RNaeBfXDo+YCG7ueslLmjCzcjKjAr2QGdhfLnEdx/Ozn8CnMLOj+2PQ4rrfZ2ijzvv7dUNnbOs36DTrxbNys0BEQu0MhAoMzX6xAecDzd+FNnc+/+TK/xQ2pDZjxTYdwitJF0szdErUt11NzK85QNBL0JjCDWHurn: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..ae3ffe9c 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": { @@ -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/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 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

- str: 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 = "" @@ -92,8 +95,10 @@ 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()) + _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()) @@ -121,7 +126,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) @@ -157,7 +168,7 @@ def sign( :param plain_dict: The payload of JWS. :type plain_dict: Union[dict, str, int, None] - :param protected: a dict containing all the values + :param protected: a dict containing all the values to include in the protected header. :type protected: dict :param kwargs: Other optional fields to generate the JWE. diff --git a/pyeudiw/jwt/exceptions.py b/pyeudiw/jwt/exceptions.py index f9428711..dcc5b45f 100644 --- a/pyeudiw/jwt/exceptions.py +++ b/pyeudiw/jwt/exceptions.py @@ -5,4 +5,7 @@ class JWTInvalidElementPosition(Exception): pass class JWSVerificationError(Exception): - pass \ No newline at end of file + pass + +class JWEEncryptionError(Exception): + pass 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): 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/satosa/backend.py b/pyeudiw/satosa/backend.py index 7ee06800..a00ec60c 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]]]: diff --git a/pyeudiw/sd_jwt/__init__.py b/pyeudiw/sd_jwt/__init__.py index 14da89ff..3855623b 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,12 +108,39 @@ def import_pyca_pri_rsa(key, **params): ) return jwcrypto.jwk.JWK(**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=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) + ) + 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))) @@ -138,7 +166,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 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 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/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/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/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", 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) - diff --git a/pyeudiw/tests/satosa/test_backend.py b/pyeudiw/tests/satosa/test_backend.py index a348a4a4..03a4be00 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 @@ -183,7 +183,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], ) @@ -336,7 +336,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], ) @@ -480,7 +480,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 = [ 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) + + diff --git a/pyeudiw/tests/test_jwt.py b/pyeudiw/tests/test_jwt.py index bd9373fc..e89276ed 100644 --- a/pyeudiw/tests/test_jwt.py +++ b/pyeudiw/tests/test_jwt.py @@ -19,11 +19,8 @@ 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) +@pytest.mark.parametrize("jwk, payload", JWKs) def test_decode_jwt_header(jwk, payload): jwe_helper = JWEHelper(jwk) jwe = jwe_helper.encrypt(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) @@ -50,7 +47,7 @@ def test_jwe_helper_encrypt(jwk, payload): assert is_jwe_format(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) @@ -61,7 +58,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) @@ -78,13 +75,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) @@ -95,7 +93,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)