Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cff343d
tests: added test
PascalDR Mar 7, 2025
57589c1
fix: specialized exception
PascalDR Mar 7, 2025
60e2740
docs: updated docstring
PascalDR Mar 7, 2025
033991f
fix: missing import
PascalDR Mar 7, 2025
cbe763e
feat: metadata param
PascalDR Mar 7, 2025
e665f5e
feat: add metadata in authorization request
PascalDR Mar 7, 2025
f7eb1a0
feat: direct_post input normalization (#384)
Zicchio Mar 10, 2025
00498e9
fix: ensure that only public keys are disclosed
PascalDR Mar 10, 2025
3e4ceae
fix: handle the remote case when the jwt is directly added in the Tru…
PascalDR Mar 10, 2025
c4dcd0b
tests: adapted test
PascalDR Mar 10, 2025
1cb412f
fix: prevent private key in metadata cleaning the keys
PascalDR Mar 10, 2025
e1b32ed
fix: clean metadata jwks
PascalDR Mar 10, 2025
90d7f92
fix: clean metadata jwk in get_metadata
PascalDR Mar 10, 2025
03b1835
tests: adapted tests
PascalDR Mar 10, 2025
68246d2
Merge pull request #386 from italia/feat/client_metadata
peppelinux Mar 10, 2025
239c50a
Merge pull request #385 from italia/tests/pubkey_test
peppelinux Mar 10, 2025
88cac52
Merge branch 'dev' of https://github.com/italia/eudi-wallet-it-python…
PascalDR Mar 11, 2025
f4073e6
feat: added specialized exception
PascalDR Mar 11, 2025
eb7d313
fix: added methods for validation
PascalDR Mar 11, 2025
aac9687
fix: check format in response handler to distinguish Mdoc or SDJWT
PascalDR Mar 11, 2025
b4b299e
fix: jwt exp
PascalDR Mar 11, 2025
065fa8d
feat: added initial mdoc support
PascalDR Mar 11, 2025
82eeb54
tests: fix error messages
PascalDR Mar 11, 2025
d8f41e6
Merge branch 'feat/x509_handler' of https://github.com/italia/eudi-wa…
PascalDR Mar 11, 2025
92fd9a9
fix: documents path
PascalDR Mar 11, 2025
507ae7a
fix: function name
PascalDR Mar 11, 2025
b824455
fix: only verify signature
PascalDR Mar 11, 2025
6d351c0
test: test mdoc too
PascalDR Mar 11, 2025
d06655e
Apply suggestions from code review
peppelinux Mar 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pyeudiw/openid4vp/authorization_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def build_authorization_request_claims(
response_uri: str,
authorization_config: dict,
nonce: str = "",
metadata: dict = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
metadata: dict = None,
metadata: dict = {},

) -> dict:
"""
Primitive function to build the payload claims of the (JAR) authorization request.
Expand All @@ -41,6 +42,8 @@ def build_authorization_request_claims(
:param nonce: optional nonce to be inserted in the request object; if not \
set, a new cryptographically safe uuid v4 nonce is generated.
:type nonce: str
:param metadata: optional metadata to be included in the request object
:type metadata: dict
:raises KeyError: if authorization_config misses mandatory configuration options
:returns: a dictionary with the *complete* set of jar jwt playload claims
:rtype: dict
Expand All @@ -62,6 +65,10 @@ def build_authorization_request_claims(
"iat": iat_now(),
"exp": exp_from_now(minutes=authorization_config["expiration_time"]),
}

if metadata:
claims["client_metadata"] = metadata

if authorization_config.get("scopes"):
claims["scope"] = " ".join(authorization_config["scopes"])
# backend configuration validation should check that at least PE or DCQL must be configured within the authz request conf
Expand Down
33 changes: 32 additions & 1 deletion pyeudiw/openid4vp/authorization_response.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import TypeVar
import cryptojwt.jwe.exception
import satosa.context
from pyeudiw.jwt.exceptions import JWEDecryptionError
Expand All @@ -14,6 +15,27 @@
)


_S = TypeVar('_S', str, list[str])


def normalize_jsonstring_to_string(s: _S) -> _S:
"""
Normalize s from string (or list of string) or JSON String (or list
of JSON String) to simply string (or list of string).
For example, this would map a vp_token from JSON String "ey...Ui5" to
the naitve string ey...Ui5 (note the missing quote ").

Note that this method is NOT intended to parse JSON String.
For that purpose, json.loads should be preferred. Instead, this method
should be used when an imput might be a string OR a JSON string.
"""
if isinstance(s, str):
return s.strip('"')
if isinstance(s, list):
return [v.strip('"') for v in s]
return s


def detect_response_mode(context: satosa.context.Context) -> ResponseMode:
"""
Try to make inference on which response mode type this is based on the
Expand Down Expand Up @@ -67,7 +89,16 @@ def parse_and_validate(

resp_data: dict = context.request
try:
return AuthorizeResponsePayload(**resp_data)
d = {}
if (vp_token := resp_data.get("vp_token", None)):
# vp_token should be a JSON string but caller might not be compliant and use string instead
vp_token = normalize_jsonstring_to_string(vp_token)
d["vp_token"] = vp_token
if (state := resp_data.get("state", None)):
d["state"] = state
if (presentation_submission := resp_data["presentation_submission"]):
d["presentation_submission"] = presentation_submission
return AuthorizeResponsePayload(**d)
except Exception as e:
raise AuthRespParsingException(
"invalid data in direct_post request body", e
Expand Down
12 changes: 12 additions & 0 deletions pyeudiw/openid4vp/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,16 @@ class MissingIssuer(Exception):
"""
Raised when a given VP not contain the issuer
"""
pass

class MdocCborValidationError(Exception):
"""
Raised when a given VP not contain the issuer
"""
pass

class VPExpired(Exception):
"""
Raised when a given VP is expired
"""
pass
35 changes: 29 additions & 6 deletions pyeudiw/openid4vp/vp_mdoc_cbor.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
from pymdoccbor.mdoc.verifier import MdocCbor
from datetime import datetime
from pyeudiw.openid4vp.exceptions import MdocCborValidationError
import logging

from pyeudiw.openid4vp.vp import Vp
logger = logging.getLogger(__name__)


class VpMDocCbor(Vp):
class VpMDocCbor:
def __init__(self, data: str) -> None:
self.data = data
self.mdoc = MdocCbor()
self.parse_digital_credential()

def parse_digital_credential(self) -> None:
self.mdoc.load(data=self.data)
def get_documents(self) -> dict:
return self.mdoc.data_as_cbor_dict["documents"]

def is_revoked(self) -> bool:
return False

def is_expired(self) -> bool:
_val_until: str = ""
try:
_val_until = self.mdoc.data_as_cbor_dict()["issuerSigned"]["issuerAuth"]["validityInfo"].get("validUntil")
except KeyError as e:
logger.error(f'Unconsitent issuerSigned schema ["issuerSigned"]["issuerAuth"]["validityInfo"], {e}, in mdoc cbor: {self.mdoc.data_as_cbor_dict()}')
if _val_until:
exp_date = datetime.fromisoformat(_val_until)
else:
logger.warning(f"Missing issuerSigned velidUntil in mdoc cbor: {self.mdoc.data_as_cbor_dict()}")

def verify(self, **kwargs) -> bool:
return self.mdoc.verify()
return exp_date < datetime.now()

def verify_signature(self) -> None:
if self.mdoc.verify() == False:
raise MdocCborValidationError("Signature is invalid")

def parse_digital_credential(self) -> None:
self.mdoc.loads(data=self.data)

def _detect_vp_type(self) -> str:
return "mdoc_cbor"
10 changes: 3 additions & 7 deletions pyeudiw/openid4vp/vp_sd_jwt_vc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

from pyeudiw.jwt.helper import is_jwt_expired
from pyeudiw.openid4vp.interface import VpTokenParser, VpTokenVerifier
from pyeudiw.sd_jwt.schema import VerifierChallenge, is_sd_jwt_kb_format
from pyeudiw.sd_jwt.schema import VerifierChallenge
from pyeudiw.sd_jwt.sd_jwt import SdJwt
from pyeudiw.openid4vp.exceptions import NotKBJWT, MissingIssuer
from pyeudiw.openid4vp.exceptions import MissingIssuer


class VpVcSdJwtParserVerifier(VpTokenParser, VpTokenVerifier):
Expand All @@ -18,10 +18,6 @@ def __init__(
verifier_nonce: Optional[str] = None,
):
self.token = token
if not is_sd_jwt_kb_format(token):
raise NotKBJWT(
f"input [token]={token} is not an sd-jwt with key binding: maybe it is a regular jwt or key binding jwt is missing?"
)
self.verifier_id = verifier_id
self.verifier_nonce = verifier_nonce
# precomputed values
Expand Down Expand Up @@ -64,7 +60,7 @@ def is_expired(self) -> bool:
:returns: if the credential is expired
:rtype: bool
"""
return is_jwt_expired(self.sdjwt.issuer_jwt)
return is_jwt_expired(self.sdjwt.issuer_jwt.jwt)

def verify_signature(self, public_key: ECKey | RSAKey | dict) -> None:
"""
Expand Down
6 changes: 6 additions & 0 deletions pyeudiw/satosa/default/request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@ def request_endpoint(self, context: Context, *args) -> Response:
"request error: missing or invalid parameter [id]",
e400
)

try:
metadata = self.trust_evaluator.get_metadata(self.client_id)
except Exception:
metadata = None

data = build_authorization_request_claims(
self.client_id,
state,
self.absolute_response_url,
self.config["authorization"],
metadata=metadata,
)

if _aud := self.config["authorization"].get("aud"):
Expand Down
Loading