Skip to content

Commit 73c4c2c

Browse files
authored
Merge pull request #428 from Zicchio/feat/jwt_trust_header_inclusion
trust header (including x5c) rework
2 parents 77d48bf + 396727b commit 73c4c2c

File tree

16 files changed

+276
-97
lines changed

16 files changed

+276
-97
lines changed

example/satosa/pyeudiw_backend.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ config:
209209
httpc_params: *httpc_params
210210
cache_ttl: 0
211211
entity_configuration_exp: 600
212+
# include_issued_jwt_header_param: true # default false; if true, it will include trust_chain header parameters in the signed presentation request issued by this trust handler
212213
metadata_type: "openid_credential_verifier"
213214
metadata: *metadata
214215
authority_hints:
@@ -248,6 +249,7 @@ config:
248249
config:
249250
# client_id: *client_id
250251
client_id_scheme: x509_san_dns # this will be prepended in the client id scheme used in the request.
252+
include_issued_jwt_header_param: true # default false; if true, it will include x5c header parameters in the signed presentation request issued by this trust handler
251253
certificate_authorities:
252254
ca.example.com: |
253255
-----BEGIN CERTIFICATE-----

pyeudiw/jwk/parse.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import base64
12
from cryptojwt.jwk.ec import import_ec_key, ECKey
23
from cryptojwt.jwk.rsa import RSAKey, import_rsa_key
34
from ssl import DER_cert_to_PEM_cert
@@ -63,6 +64,15 @@ def parse_certificate(cert: str | bytes) -> JWK:
6364

6465
return parse_pem(cert)
6566

67+
68+
def parse_b64der(b64der: str) -> JWK:
69+
"""
70+
Parse a (public) key from a Base64 encoded DER certificate.
71+
"""
72+
der = base64.b64decode(b64der)
73+
return parse_certificate(der)
74+
75+
6676
def parse_x5c_keys(x5c: list[str]) -> list[JWK]:
6777
"""
6878
Parse a the keys from a x5c chain.

pyeudiw/satosa/default/request_handler.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
from pyeudiw.satosa.interfaces.request_handler import RequestHandlerInterface
66
from pyeudiw.satosa.utils.response import Response
77
from pyeudiw.tools.base_logger import BaseLogger
8-
from pyeudiw.jwt.exceptions import JWSSigningError
9-
from pyeudiw.jwk.parse import parse_certificate
8+
from pyeudiw.jwk.parse import parse_b64der
109
from pyeudiw.jwk import JWK
1110

1211

@@ -75,7 +74,8 @@ def request_endpoint(self, context: Context, *args) -> Response:
7574
metadata_key = None
7675

7776
if "x5c" in _protected_jwt_headers:
78-
jwk = parse_certificate(_protected_jwt_headers["x5c"][0])
77+
# TODO: move this logic in the JWS signer...
78+
jwk = parse_b64der(_protected_jwt_headers["x5c"][0])
7979

8080
for key in self.config["metadata_jwks"]:
8181
if JWK(key).thumbprint == jwk.thumbprint:
@@ -98,6 +98,7 @@ def request_endpoint(self, context: Context, *args) -> Response:
9898
data,
9999
protected=_protected_jwt_headers,
100100
)
101+
self._log_debug(context, f"created request object {request_object_jwt}")
101102
return Response(
102103
message=request_object_jwt,
103104
status="200",

pyeudiw/tests/federation/base.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@
2929
leaf_cred = {
3030
"exp": EXP,
3131
"iat": NOW,
32-
"iss": "https://credential_issuer.example.org",
33-
"sub": "https://credential_issuer.example.org",
32+
"iss": "https://credential-issuer.example.org",
33+
"sub": "https://credential-issuer.example.org",
3434
"jwks": {"keys": []},
3535
"metadata": {
3636
"openid_credential_issuer": {"jwks": {"keys": []}},
3737
"federation_entity": {
3838
"organization_name": "OpenID Credential Issuer example",
39-
"homepage_uri": "https://credential_issuer.example.org/home",
40-
"policy_uri": "https://credential_issuer.example.org/policy",
41-
"logo_uri": "https://credential_issuer.example.org/static/logo.svg",
42-
"contacts": ["tech@credential_issuer.example.org"],
39+
"homepage_uri": "https://credential-issuer.example.org/home",
40+
"policy_uri": "https://credential-issuer.example.org/policy",
41+
"logo_uri": "https://credential-issuer.example.org/static/logo.svg",
42+
"contacts": ["tech@credential-issuer.example.org"],
4343
},
4444
},
4545
"authority_hints": ["https://intermediate.eidas.example.org"],
@@ -55,7 +55,7 @@
5555
"exp": EXP,
5656
"iat": NOW,
5757
"iss": "https://intermediate.eidas.example.org",
58-
"sub": "https://credential_issuer.example.org",
58+
"sub": "https://credential-issuer.example.org",
5959
"jwks": {"keys": []},
6060
}
6161
intermediate_es_cred["jwks"]["keys"] = [leaf_cred_jwk.serialize()]

pyeudiw/tests/satosa/test_backend.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,6 @@ def test_response_endpoint(self, context):
467467

468468
# case (4): good aud, nonce and state
469469
good_response = self._generate_payload(self.issuer_jwk, self.holder_jwk, nonce, state, self.backend.client_id)
470-
471470
encrypted_response = JWEHelper(
472471
CONFIG["metadata_jwks"][1]).encrypt(good_response)
473472
context.request = {
@@ -897,7 +896,7 @@ def test_request_endpoint(self, context):
897896
# msg = json.loads(state_endpoint_response.message)
898897
# assert msg["response"] == "Authentication successful"
899898

900-
def test_trust_patameters_in_response(self, context):
899+
def test_trust_parameters_in_response(self, context):
901900
internal_data = InternalData()
902901
context.http_headers = dict(
903902
HTTP_USER_AGENT="Mozilla/5.0 (Linux; Android 10; SM-G960F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.92 Mobile Safari/537.36"
@@ -913,12 +912,13 @@ def test_trust_patameters_in_response(self, context):
913912

914913
tsd = TrustSourceData.empty(CREDENTIAL_ISSUER_ENTITY_ID)
915914
tsd.add_trust_param(
916-
"trust_chain",
915+
"federation",
917916
TrustEvaluationType(
918917
attribute_name="trust_chain",
919918
jwks=[JWK(key=ta_jwk).as_dict()],
920919
expiration_date=datetime.datetime.now(),
921920
trust_chain=trust_chain_wallet,
921+
trust_handler_name="FederationHandler",
922922
)
923923
)
924924

pyeudiw/tests/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ def base64url_to_int(val):
268268
ta_jwk.serialize(private=False),
269269
]
270270
},
271+
"include_issued_jwt_header_param": True,
271272
"default_sig_alg": "RS256",
272273
"federation_jwks": [
273274
jwk,
@@ -310,6 +311,7 @@ def base64url_to_int(val):
310311
"class": "X509Handler",
311312
"config": {
312313
"client_id": f"{BASE_URL}/OpenID4VP",
314+
"include_issued_jwt_header_param": True,
313315
"relying_party_certificate_chains_by_ca": {
314316
"ca.example.com": DEFAULT_X509_CHAIN,
315317
},

pyeudiw/tests/trust/handler/test_direct_trust.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
issuer,
1212
)
1313
from pyeudiw.tests.trust.handler import issuer_jwk as expected_jwk
14-
from pyeudiw.trust.handler._direct_trust_jwk import build_jwk_issuer_endpoint
14+
from pyeudiw.trust.handler._direct_trust_jwk import build_jwk_issuer_endpoint, is_url
1515
from pyeudiw.trust.handler.direct_trust_sd_jwt_vc import (
1616
DirectTrustSdJwtVc,
1717
build_metadata_issuer_endpoint,
@@ -24,7 +24,7 @@
2424
def fake_get_http_url(
2525
urls: list[str] | str, httpc_params: dict, http_async: bool = True
2626
) -> list[requests.Response]:
27-
issuer = f"https://example_url.issuer.it/vct"
27+
issuer = f"https://example-url.issuer.it/vct"
2828

2929
if urls[0].endswith("vct"):
3030
response = Response()
@@ -112,7 +112,7 @@ def test_direct_trust_extract_jwks_from_jwk_metadata_invalid():
112112
def test_direct_trust_jwk():
113113
trust_handler = DirectTrustSdJwtVc()
114114

115-
random_issuer = f"{uuid.uuid4()}.issuer.it"
115+
random_issuer = f"https://{uuid.uuid4()}.issuer.it"
116116

117117
mocked_issuer_jwt_vc_issuer_endpoint = unittest.mock.patch(
118118
"pyeudiw.trust.handler._direct_trust_jwk.get_http_url",
@@ -143,7 +143,7 @@ def test_direct_trust_jwk():
143143
def test_direct_trust_jwk_not_conformat_url():
144144
trust_handler = DirectTrustSdJwtVc()
145145

146-
issuer = f"https://example_url.issuer.it/vct"
146+
issuer = f"https://example-url.issuer.it/vct"
147147

148148
mocked_issuer_jwt_vc_issuer_endpoint = unittest.mock.patch(
149149
"pyeudiw.trust.handler._direct_trust_jwk.get_http_url",
@@ -163,3 +163,12 @@ def test_direct_trust_jwk_not_conformat_url():
163163

164164
assert len(obtained_jwks) == 1, f"expected 1 jwk, obtained {len(obtained_jwks)}"
165165
assert expected_jwk == obtained_jwks[0]
166+
167+
168+
def test_is_url():
169+
assert is_url("missing-scheme.net") == False
170+
assert is_url("http//malformed-scheme.net") == False
171+
assert is_url("https://malformed_domain.org") == False
172+
assert is_url("https://domain.example") == True
173+
assert is_url("https://domain.example/path") == True
174+
assert is_url("https://domain.example/path/trailing/") == True

pyeudiw/tests/trust/mock_trust_handler.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class MockTrustHandler(TrustHandlerInterface):
3232
def __init__(self, *args, **kwargs):
3333
self.client_id = kwargs.get("default_client_id", None)
3434
self.exp = kwargs.get("exp", 10)
35+
self.include_issued_jwt_header_param = kwargs.get("include_issued_jwt_header_param", False)
3536

3637
def get_metadata(self, issuer: str, trust_source: TrustSourceData) -> dict:
3738
if issuer == self.client_id:
@@ -75,6 +76,9 @@ def extract_and_update_trust_materials(
7576
trust_source.add_trust_param("test_trust_param", trust_param)
7677

7778
return trust_source
79+
80+
def extract_jwt_header_trust_parameters(self, trust_source: TrustSourceData) -> dict:
81+
return {'trust_param_name': trust_source.test_trust_param.trust_param_name}
7882

7983
class UpdateTrustHandler(MockTrustHandler):
8084
"""
@@ -108,6 +112,9 @@ def extract_and_update_trust_materials(
108112

109113
return trust_source
110114

115+
def extract_jwt_header_trust_parameters(self, trust_source: TrustSourceData) -> dict:
116+
return {'trust_param_name': trust_source.test_trust_param.trust_param_name}
117+
111118
class NonConformatTrustHandler:
112119
def get_metadata(self, issuer: str, trust_source: TrustSourceData) -> dict:
113120
return trust_source

pyeudiw/tests/trust/test_dynamic.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,13 @@ def test_public_key_and_metadata_retrive():
5252
"mock": {
5353
"module": "pyeudiw.tests.trust.mock_trust_handler",
5454
"class": "MockTrustHandler",
55-
"config": {},
55+
"config": {"include_issued_jwt_header_param": True},
5656
},
5757

5858
}, db_engine, default_client_id="default-client-id", mode="update_first"
5959
)
6060

6161
uuid_url = f"http://{uuid4()}.issuer.it"
62-
6362
assert trust_ev.get_jwt_header_trust_parameters(uuid_url) == {'trust_param_name': {'trust_param_key': 'trust_param_value'}}
6463
metadata = trust_ev.get_metadata()
6564

@@ -82,7 +81,7 @@ def test_update_first_strategy():
8281
"mock": {
8382
"module": "pyeudiw.tests.trust.mock_trust_handler",
8483
"class": "UpdateTrustHandler",
85-
"config": {},
84+
"config": {"include_issued_jwt_header_param": True},
8685
},
8786

8887
}, db_engine, default_client_id="default-client-id"
@@ -102,7 +101,7 @@ def test_cache_first_strategy():
102101
"mock": {
103102
"module": "pyeudiw.tests.trust.mock_trust_handler",
104103
"class": "UpdateTrustHandler",
105-
"config": {},
104+
"config": {"include_issued_jwt_header_param": True},
106105
},
107106

108107
}, db_engine, default_client_id="default-client-id", mode="cache_first"
@@ -122,7 +121,8 @@ def test_cache_first_strategy_expired():
122121
"module": "pyeudiw.tests.trust.mock_trust_handler",
123122
"class": "UpdateTrustHandler",
124123
"config": {
125-
"exp": 0
124+
"exp": 0,
125+
"include_issued_jwt_header_param": True
126126
},
127127
},
128128

@@ -143,7 +143,7 @@ def test_cache_first_strategy_expired_revoked():
143143
"mock": {
144144
"module": "pyeudiw.tests.trust.mock_trust_handler",
145145
"class": "UpdateTrustHandler",
146-
"config": {},
146+
"config": {"include_issued_jwt_header_param": True},
147147
},
148148

149149
}, db_engine, default_client_id="default-client-id", mode="cache_first"
@@ -166,7 +166,7 @@ def test_cache_first_strategy_expired_force_update():
166166
"mock": {
167167
"module": "pyeudiw.tests.trust.mock_trust_handler",
168168
"class": "UpdateTrustHandler",
169-
"config": {},
169+
"config": {"include_issued_jwt_header_param": True},
170170
},
171171

172172
}, db_engine, default_client_id="default-client-id", mode="cache_first"

pyeudiw/trust/dynamic.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
UpsertMode = Union[Literal["update_first"], Literal["cache_first"]]
2121

22+
INCLUDE_JWT_HEADER_CONFIG_NAME = "include_issued_jwt_header_param"
23+
2224

2325
class CombinedTrustEvaluator(BaseLogger):
2426
"""
@@ -194,10 +196,7 @@ def get_public_keys(
194196
else:
195197
used_handlers.append(handler.__class__.__name__)
196198

197-
raise NoCriptographicMaterial(
198-
f"no trust evaluator can provide cyptographic material "
199-
f"for {issuer}: searched among: {self.handlers_names}"
200-
)
199+
self._log_warning("static trust evaluation", f"no configured trust handler can successfully process static trust material {static_trust_materials} of issuer {issuer}")
201200

202201
# try with handlers that don't use static trust materials like DirectTrustJar
203202
filetered_handlers = [handler for handler in self.handlers if handler.__class__.__name__ not in used_handlers]
@@ -303,14 +302,11 @@ def get_jwt_header_trust_parameters(self, issuer: Optional[str] = None, force_up
303302
"""
304303
trust_source = self._get_trust_source(issuer, force_update)
305304

306-
excluded_fields = ["entity_id", "policies", "metadata", "revoked"]
307-
308305
headers_params = {}
309-
310-
for param_name, param_value in trust_source.serialize().items():
311-
if param_name not in excluded_fields:
312-
headers_params[param_value["attribute_name"]] = param_value[param_value["attribute_name"]]
313-
306+
for handler in self.handlers:
307+
if getattr(handler, INCLUDE_JWT_HEADER_CONFIG_NAME, None):
308+
if header := handler.extract_jwt_header_trust_parameters(trust_source):
309+
headers_params.update(header)
314310
return headers_params
315311

316312
def build_metadata_endpoints(

0 commit comments

Comments
 (0)