Skip to content

Commit 02e0e64

Browse files
committed
- fixes/cleanup
1 parent 071fba7 commit 02e0e64

3 files changed

Lines changed: 59 additions & 35 deletions

File tree

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,10 @@ PYEUDIW_MONGO_TEST_AUTH_INLINE=satosa:thatpassword@ pytest pyeudiw -x
184184

185185
Your contribution is welcome, no question is useless and no answer is obvious, we need you.
186186

187-
188187
### Contribute as end user
189188

190189
Please open an issue if you've found a bug or if you want to ask some features.
191190

192-
193191
### Contribute as developer
194192

195193
Please open your Pull Requests on the __dev__ branch.
@@ -248,7 +246,6 @@ PYEUDIW_MONGO_TEST_AUTH_INLINE="satosa:thatpassword@" pytest pyeudiw -x
248246

249247
## Acknowledgments
250248

251-
- Manuel Pacella
252249
- Manuel Ciofo
253250
- Thomas Chiozzi
254251
- Pasquale De Rose

pyeudiw/satosa/frontends/openid4vci/endpoints/base_credential_endpoint.py

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import datetime
21
import time
32
import logging
43
import inspect
4+
import base64
5+
import cbor2
6+
7+
from pycose.keys import CoseKey
8+
from pyeudiw.jwk import JWK
59
from pyeudiw.satosa.schemas.credential_configurations import CredentialConfigurationsConfig
610
from pyeudiw.storage.exceptions import EntryNotFound
711
from abc import ABC, abstractmethod
@@ -157,17 +161,12 @@ def endpoint(self, context: Context) -> Response:
157161
if data.get("credential_configuration_id"):
158162
credential_configuration_id = data
159163
else: # validate credential_identifier with authorization_details of token
160-
if auth_session.authorization_details:
161-
for auth_details in auth_session.authorization_details:
162-
if auth_details.credential_identifiers:
163-
if credential_identifier in auth_details.credential_identifiers:
164-
credential_configuration_id = "_".join(credential_identifier.split("_")[:-1])
165-
break
166-
else:
167-
raise InvalidRequestException(
168-
"credential_identifier not match with token authorization_details")
164+
for auth_details in auth_session.authorization_details or []:
165+
if credential_identifier in (auth_details.credential_identifiers or []):
166+
credential_configuration_id = "_".join(credential_identifier.split("_")[:-1])
167+
break
169168
else:
170-
raise InvalidRequestException("Invalid credential_configuration_id")
169+
raise InvalidRequestException("Invalid credential_configuration_id or credential_identifier not match with token authorization_details")
171170

172171
self.validate_request(context, entity)
173172

@@ -265,8 +264,7 @@ def _build_credential(
265264
match config.format:
266265
case CredentialConfigurationFormatEnum.SD_JWT.value:
267266
return self._issue_sd_jwt(
268-
user_entity, opendid4vci_entity, cred_config, config, holder_key=holder_key
269-
)["issuance"]
267+
user_entity, opendid4vci_entity, cred_config, config, holder_key=holder_key)
270268
case CredentialConfigurationFormatEnum.MSO_MDOC.value:
271269
return self._issue_mso_mdoc(user_entity, opendid4vci_entity, cred_config, config, holder_key=holder_key)
272270
case _:
@@ -281,38 +279,67 @@ def _build_credential(
281279
def _issue_mso_mdoc(
282280
self,
283281
user_entity: tuple[str, UserEntity],
284-
auth_session: AuthorizationSession, cred_config: CredentialConfigurationsConfig,
285-
config: CredentialConfiguration, holder_key: dict = None
282+
auth_session: AuthorizationSession, cred_config: CredentialConfigurationsConfig, config: CredentialConfiguration, holder_key: dict = None
286283
) -> str:
287-
credential: CredentialSpecificationConfig = cred_config.credential_specification[config.id] #todo
284+
"""References:
285+
- https://italia.github.io/eid-wallet-it-docs/releases/1.3.3/en/credential-data-model-pid.html#pid-data-model-in-mdoc-cbor-format
286+
- https://italia.github.io/eid-wallet-it-docs/releases/1.3.3/en/credential-data-model.html#mdoc-cbor-credential-format
287+
"""
288+
288289
mdoci = MdocCborIssuer(
289290
private_key=self._mso_mdoc_private_key,
290291
alg=self._mso_mdoc_private_key["ALG"],
291292
)
292-
issuance_date = datetime.date.today()
293+
if not (x5c_chain := self._trust_evaluator.get_jwt_header_trust_parameters(issuer=self.entity_id).get("x5c", [])):
294+
raise Exception("missing x5c chain")
295+
296+
for i in range(0, len(x5c_chain)):
297+
x5c_chain[i] = b"-----BEGIN CERTIFICATE-----\n" + x5c_chain[i].encode('utf-8') + b"\n-----END CERTIFICATE-----\n"
298+
299+
user_id, _ = user_entity
300+
credential: CredentialSpecificationConfig = cred_config.credential_specification[config.id]
301+
302+
now = iat_now()
303+
exp = exp_from_now(self.config_utils.get_jwt().default_exp) # TODO implement a config section for MobileSecurityObject and the related get_mso()
304+
nbf = now + cred_config.nbf_delta
305+
required_attrs = {"issuing_country": cred_config.issuing_country, "issuing_authority": cred_config.issuing_authority}
306+
supported_optional_attrs = {
307+
"sub": str(uuid4()),
308+
"issuance_date": datetime_from_timestamp(now).strftime('%Y-%m-%dT%H:%M:%SZ'), #ISO 8601
309+
"expiry_date": (datetime_from_timestamp(now) + timedelta(credential.expiry_days)).strftime('%Y-%m-%dT%H:%M:%SZ'), # ISO 8601
310+
"trust_framework": credential.trust_framework, "assurance_level": credential.assurance_level
311+
}
312+
313+
holder_key = cbor2.loads(CoseKey.from_pem_public_key(JWK(key=holder_key).export_public_pem()).encode())
314+
status_list = None
315+
if credential.optional_mso_attrs:
316+
if credential.optional_mso_attrs.get("status"):
317+
status_list = self.revoke_on_credential_reissuance(user_id, auth_session, config.id)
318+
293319
mdoci.new(
320+
status=status_list, devicekeyinfo=holder_key, x509_chain=x5c_chain,
294321
doctype=config.doctype,
295322
data=self._loader(
296323
user_entity,
297324
credential.template,
298325
CredentialConfigurationFormatEnum.MSO_MDOC.value,
326+
extra_claims=required_attrs | supported_optional_attrs
299327
),
300328
validity={
301-
"issuance_date": issuance_date.isoformat(),
302-
"expiry_date": (
303-
issuance_date + timedelta(credential.expiry_days)
304-
).isoformat(),
329+
"issuance_date": datetime_from_timestamp(nbf).strftime("%Y-%m-%d"),
330+
"expiry_date": datetime_from_timestamp(exp).strftime("%Y-%m-%d") #pymdoccbor==1.3.0 needs iso format "%Y-%m-%d"
305331
},
306332
)
307-
return mdoci.dumps().decode()
333+
issuer_signed_data = mdoci.signed["documents"][0]['issuerSigned']
334+
data = cbor2.dumps(issuer_signed_data, canonical=True)
335+
return base64.urlsafe_b64encode(data).decode()
308336

309337
def _issue_sd_jwt(
310-
self, user_entity: tuple[str, UserEntity], auth_session: AuthorizationSession,
311-
cred_config: CredentialConfigurationsConfig, iss_cred_supp_conf: CredentialConfiguration, holder_key: dict = None
312-
) -> dict:
338+
self, user_entity: tuple[str, UserEntity], auth_session: AuthorizationSession, cred_config: CredentialConfigurationsConfig,
339+
iss_cred_supp_conf: CredentialConfiguration, holder_key: dict|None = None) -> str:
313340
"""Reference: https://italia.github.io/eid-wallet-it-docs/releases/1.3.3/en/credential-data-model.html#digital-credential-sd-jwt-metadata-attributes"""
314341
now = iat_now()
315-
exp = exp_from_now(self.config_utils.get_jwt().default_exp) # TODO check date_of_expiry
342+
exp = exp_from_now(self.config_utils.get_jwt().default_exp)
316343
cred_type_id = iss_cred_supp_conf.id
317344
cred_specification: CredentialSpecificationConfig = cred_config.credential_specification[cred_type_id]
318345
required_claims = {"iss": self.entity_id, "exp": exp, "issuing_authority": cred_config.issuing_authority, "issuing_country": cred_config.issuing_country,
@@ -340,11 +367,7 @@ def _issue_sd_jwt(
340367
issuer=self.entity_id
341368
),
342369
)
343-
344-
return {
345-
"jws": sdjwt_at_issuer.serialized_sd_jwt,
346-
"issuance": sdjwt_at_issuer.sd_jwt_issuance,
347-
}
370+
return sdjwt_at_issuer.sd_jwt_issuance
348371

349372
@staticmethod
350373
def _retrieve_user_data(
@@ -366,13 +389,16 @@ def _loader(
366389
match credential_type:
367390
case CredentialConfigurationFormatEnum.SD_JWT.value:
368391
template = Template(template)
392+
# TODO remove and generalize it to simulate data recovery from an Authentic Source
369393
user_data = self._retrieve_user_data(user_data)
370394
user_data = user_data | (extra_claims or {})
371395
json_filled = template.render(**user_data)
372396
return yaml_load_specification(json_filled)
373397
case CredentialConfigurationFormatEnum.MSO_MDOC.value:
398+
user_data = self._retrieve_user_data(user_entity) # TODO remove and generalize it to simulate data recovery from an Authentic Source
399+
data = user_data | (extra_claims or {})
374400
data = render_mso_mdoc_template(
375-
template, user_data.model_dump(), FIELD_TRANSFORMS
401+
template, data, FIELD_TRANSFORMS
376402
)
377403
return data
378404
case _:

pyeudiw/satosa/schemas/credential_specification.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ class CredentialSpecificationConfig(BaseModel):
1010

1111
template: str
1212
expiry_days: Optional[int] = None
13+
optional_mso_attrs: Optional[dict] = None
1314
trust_framework: Optional[str] = ""
1415
assurance_level: Optional[str] = ""

0 commit comments

Comments
 (0)