Skip to content

Commit

Permalink
feat: support for vc+sd-jwt (#273)
Browse files Browse the repository at this point in the history
* sd-jwt format regexp + test + linting

* wip: authn response processing

* wip: vc+sd-jwt schema

* wip: vc+sd-jwt

* wip: unit test redo

* wip: test for vc+sd-jwt

* fix: miscellaneous minor fixes

* chore: cleanup

* chore: docs, linting, cleanup

* applied some suggestions

* applied come code suggestions

* wip: ideas, brainstoming, etc

* wip: intent clarification

* wip: new trust model and vp interface

* wip: backend configuration example

* wip

* wip: stub of response with trust model and vp parser

* wip: unit tests

* chore: renamed function

* wip: unit tests, fixes

* wip: cleanup

* wip: cleanup

* wip: fixes

* wip: integration test review

* wip: patch get connection

* Apply suggestions from code review

* Apply suggestions from code review

* fix: changed file name

* Apply suggestions from code review

* wip: storage layer of trust eval

* wip: storage layer + tests fixes

* wip: integration tests

* wip: fix python 3.12 retrocompatibility + todos

* wip: exception handling

---------

Co-authored-by: elisanp <[email protected]>
Co-authored-by: elisanp <[email protected]>
Co-authored-by: Giuseppe De Marco <[email protected]>
  • Loading branch information
4 people authored Oct 18, 2024
1 parent 8299616 commit f66aa68
Show file tree
Hide file tree
Showing 59 changed files with 3,034 additions and 748 deletions.
15 changes: 13 additions & 2 deletions docs/STORAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,22 @@ This classes can be used as references while providing a custom implementation f
"federation" : {
"chain": ARRAY[EC,ES,ES],
"exp": datetime,
"update": datetime
"update": datetime,
"jwks": {
"keys": ARRAY[object]
},
},
"x509": {
"x5c": ARRAY[bytestring(DER), bytestring(DER), bytestring(DER)] -> contains public keys,
"exp": datetime
"exp": datetime,
"jwks": {
"keys": ARRAY[object]
},
},
"direct_trust_sd_jwt_vc": {
"jwks": {
"keys": ARRAY[object]
}
}
"metadata": object
}
Expand Down
109 changes: 45 additions & 64 deletions example/satosa/integration_test/commons.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import base64
from bs4 import BeautifulSoup
import datetime
import requests
import uuid
from typing import Any, Literal
from bs4 import BeautifulSoup
from sd_jwt.holder import SDJWTHolder

from pyeudiw.jwk import JWK
from pyeudiw.jwt import DEFAULT_SIG_KTY_MAP, JWEHelper, JWSHelper
from pyeudiw.jwt import DEFAULT_SIG_KTY_MAP, JWEHelper
from pyeudiw.jwt.utils import decode_jwt_payload
from pyeudiw.presentation_exchange.schemas.oid4vc_presentation_definition import PresentationDefinition
from pyeudiw.sd_jwt import (
# _adapt_keys,
import_ec,
issue_sd_jwt,
load_specification_from_yaml_string
)
from pyeudiw.storage.base_storage import TrustType
from pyeudiw.storage.db_engine import DBEngine
from pyeudiw.tests.federation.base import (
EXP,
Expand All @@ -30,10 +27,11 @@
leaf_wallet_signed,
trust_chain_issuer
)
from pyeudiw.tools.utils import iat_now, exp_from_now
from sd_jwt.holder import SDJWTHolder
from saml2_sp import saml2_request

from saml2_sp import IDP_BASEURL
from settings import (
IDP_BASEURL,
CONFIG_DB,
RP_EID,
its_trust_chain
Expand All @@ -42,25 +40,19 @@
CREDENTIAL_ISSUER_JWK = JWK(leaf_cred_jwk_prot.serialize(private=True))
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
!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"
""",
"issuer": leaf_cred['sub'],
"default_exp": 1024
"default_exp": 1024,
"key_binding": True
}
ISSUER_PRIVATE_JWK = JWK(leaf_cred_jwk.serialize(private=True))
WALLET_PRIVATE_JWK = JWK(leaf_wallet_jwk.serialize(private=True))
WALLET_PUBLIC_JWK = JWK(leaf_wallet_jwk.serialize())

Expand All @@ -71,7 +63,7 @@ def setup_test_db_engine() -> DBEngine:

def apply_trust_settings(db_engine_inst: DBEngine) -> DBEngine:
db_engine_inst.add_trust_anchor(
entity_id=ta_ec['iss'],
entity_id=ta_ec["iss"],
entity_configuration=ta_ec_signed,
exp=EXP
)
Expand All @@ -83,24 +75,32 @@ def apply_trust_settings(db_engine_inst: DBEngine) -> DBEngine:
)

db_engine_inst.add_or_update_trust_attestation(
entity_id=leaf_wallet['iss'],
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'],
entity_id=leaf_cred["iss"],
attestation=leaf_cred_signed,
exp=datetime.datetime.now().isoformat()
)

settings = ISSUER_CONF
db_engine_inst.add_or_update_trust_attestation(
entity_id=settings["issuer"],
trust_type=TrustType.DIRECT_TRUST_SD_JWT_VC,
jwks=leaf_cred_jwk_prot.serialize())
return db_engine_inst

def create_saml_auth_request() -> str:
auth_req_url = f"{saml2_request["headers"][0][1]}&idp_hinting=wallet"
return auth_req_url

def create_issuer_test_data() -> dict[Literal["jws"] | Literal["issuance"], str]:
# create a SD-JWT signed by a trusted credential issuer
settings = ISSUER_CONF
settings['issuer'] = leaf_cred['iss']
settings['default_exp'] = 33
settings["default_exp"] = 33
sd_specification = load_specification_from_yaml_string(
settings["sd_specification"]
)
Expand All @@ -110,73 +110,54 @@ def create_issuer_test_data() -> dict[Literal["jws"] | Literal["issuance"], str]
settings,
CREDENTIAL_ISSUER_JWK,
WALLET_PUBLIC_JWK,
trust_chain=trust_chain_issuer
additional_headers={"typ": "vc+sd-jwt"}
)
return issued_jwt


def create_holder_test_data(issued_jwt: dict[Literal["jws"] | Literal["issuance"], str], request_nonce: str) -> str:
def create_holder_test_data(issued_jwt: dict[Literal["jws"] | Literal["issuance"], str], request_nonce: str, request_aud: str) -> str:
settings = ISSUER_CONF
sd_specification = load_specification_from_yaml_string(
settings["sd_specification"]
)

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'
"tax_id_code": True,
"given_name": True,
"family_name": True
},
nonce=str(uuid.uuid4()),
aud=str(uuid.uuid4()),
nonce=request_nonce,
aud=request_aud,
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)
if settings.get("key_binding", False)
else None
)
)

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": request_nonce,
"vp": sdjwt_at_holder.sd_jwt_presentation,
}

vp_token = JWSHelper(WALLET_PRIVATE_JWK).sign(
data,
protected={"typ": "JWT"}
)
vp_token = sdjwt_at_holder.sd_jwt_presentation
return vp_token


def create_authorize_response(vp_token: str, state: str, nonce: str, response_uri: str) -> str:
# take relevant information from RP's entity configuration
def create_authorize_response(vp_token: str, state: str, response_uri: str) -> str:
# Extract public key from RP's entity configuration
client = requests.Session()
rp_ec_jwt = client.get(
f'{IDP_BASEURL}/OpenID4VP/.well-known/openid-federation',
f"{IDP_BASEURL}/OpenID4VP/.well-known/openid-federation",
verify=False
).content.decode()
rp_ec = decode_jwt_payload(rp_ec_jwt)

presentation_definition = rp_ec["metadata"]["wallet_relying_party"]["presentation_definition"]
PresentationDefinition(**presentation_definition)
assert response_uri == rp_ec["metadata"]['wallet_relying_party']["response_uris_supported"][0]
assert response_uri == rp_ec["metadata"]["wallet_relying_party"]["response_uris_supported"][0]
encryption_key = rp_ec["metadata"]["wallet_relying_party"]["jwks"]["keys"][1]

response = {
"state": state,
"nonce": nonce,
"vp_token": vp_token,
"presentation_submission": {
"definition_id": "32f54163-7166-48f1-93d8-ff217bdb0653",
Expand All @@ -192,8 +173,8 @@ def create_authorize_response(vp_token: str, state: str, nonce: str, response_ur
}
}
encrypted_response = JWEHelper(
# RSA (EC is not fully supported todate)
JWK(rp_ec["metadata"]['wallet_relying_party']['jwks']['keys'][1])
# RSA (EC is not fully supported to date)
JWK(encryption_key)
).encrypt(response)
return encrypted_response

Expand Down
Loading

0 comments on commit f66aa68

Please sign in to comment.