From 403992fe679db45ea3233c54ae8c117fa50c9d0a Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 14 Aug 2023 17:18:17 -0600 Subject: [PATCH 01/68] feat: add wallet_sd_jwt_sign endpoint Signed-off-by: Char Howland --- aries_cloudagent/wallet/routes.py | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 20dd8727c1..66711ab8e5 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -50,6 +50,7 @@ from ..resolver.base import ResolverError from ..storage.error import StorageError, StorageNotFoundError from ..wallet.jwt import jwt_sign, jwt_verify +from ..wallet.sd_jwt import sd_jwt_sign from .base import BaseWallet from .did_info import DIDInfo from .did_method import KEY, SOV, DIDMethod, DIDMethods, HolderDefinedDid @@ -941,6 +942,43 @@ async def wallet_jwt_sign(request: web.BaseRequest): return web.json_response(jws) +@docs( + tags=["wallet"], summary="Create a EdDSA sd-jws using did keys with a given payload" +) +@request_schema(JWSCreateSchema) # check +@response_schema(WalletModuleResponseSchema(), description="") +async def wallet_sd_jwt_sign(request: web.BaseRequest): + """ + Request handler for sd-jws creation using did. + + Args: + "headers": { ... }, + "payload": { ... }, + "did": "did:example:123", + "verificationMethod": "did:example:123#keys-1" + with did and verification being mutually exclusive. + """ + context: AdminRequestContext = request["context"] + body = await request.json() + did = body.get("did") + verification_method = body.get("verificationMethod") + headers = body.get("headers", {}) + payload = body.get("payload", {}) + + try: + sd_jws = await sd_jwt_sign( + context.profile, headers, payload, did, verification_method + ) + except ValueError as err: + raise web.HTTPBadRequest(reason="Bad did or verification method") from err + except WalletNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + except WalletError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + return web.json_response(sd_jws) + + @docs(tags=["wallet"], summary="Verify a EdDSA jws using did keys with a given JWS") @request_schema(JWSVerifySchema()) @response_schema(JWSVerifyResponseSchema(), 200, description="") @@ -1125,6 +1163,7 @@ async def register(app: web.Application): web.post("/wallet/set-did-endpoint", wallet_set_did_endpoint), web.post("/wallet/jwt/sign", wallet_jwt_sign), web.post("/wallet/jwt/verify", wallet_jwt_verify), + web.post("/wallet/sd-jwt/sign", wallet_sd_jwt_sign), web.get( "/wallet/get-did-endpoint", wallet_get_did_endpoint, allow_head=False ), From c753d551635a75b2018417d20ac0bcc6934fecad Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 14 Aug 2023 17:20:24 -0600 Subject: [PATCH 02/68] feat: (WIP) sd_jwt_sign logic using json paths Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 103 ++++++++++++++++++++++++++++++ requirements.txt | 33 ++++++++++ 2 files changed, 136 insertions(+) create mode 100644 aries_cloudagent/wallet/sd_jwt.py create mode 100644 requirements.txt diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py new file mode 100644 index 0000000000..d35a1bd25d --- /dev/null +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -0,0 +1,103 @@ +"""Operations supporting SD-JWT creation and verification.""" + +import json +from typing import Any, List, Mapping, Optional +from jsonpath_ng.ext import parse +from sd_jwt.common import SDObj +from sd_jwt.issuer import SDJWTIssuer + +from ..core.profile import Profile +from ..wallet.jwt import jwt_sign +from ..core.error import BaseError + + +class SDJWTError(BaseError): + """SD-JWT Error""" + + +class SDJWTIssuerACAPy(SDJWTIssuer): + def __init__( + self, + user_claims, + issuer_key, + holder_key, + profile: Profile, + headers: dict, + did: Optional[str] = None, + verification_method: Optional[str] = None, + ): + super().__init__(user_claims, issuer_key, holder_key) + self.profile = profile + self.headers = headers + self.did = did + self.verification_method = verification_method + + def _create_signed_jws(self): + self.serialized_sd_jwt = "" + + return jwt_sign( + self.profile, + self.headers, + self.sd_jwt_payload, + self.did, + self.verification_method, + ) + + +def sort_sd_list(sd_list): + """ + Sorts sd list such that selectively disclosable claims deepest + in the structure are handled first. + """ + nested_claim_sort = [(len(sd.split(".")), sd) for sd in sd_list] + nested_claim_sort.sort(reverse=True) + return [sd[1] for sd in nested_claim_sort] + + +async def sd_jwt_sign( + profile: Profile, + headers: Mapping[str, Any], + payload: Mapping[str, Any], + sd_list: List, + did: Optional[str] = None, + verification_method: Optional[str] = None, +) -> str: + + sorted_sd_list = sort_sd_list(sd_list) + for sd in sorted_sd_list: + jsonpath_expression = parse(f"$.{sd}") + matches = jsonpath_expression.find(payload) + if len(matches) < 1: + raise SDJWTError("Claim for {sd} not found in payload.") + else: + for match in matches: + if type(match.context.value) is list: + match.context.value.remove(match.value) + match.context.value.append(SDObj(match.value)) + elif type(match.context.value) is str or int or dict or bool: + match.context.value[ + SDObj(str(match.path)) + ] = match.context.value.pop(str(match.path)) + else: + raise SDJWTError( + f"Unrecognized type {type(match.context.value)} for {match.path}" + ) + + sd_jwt_issuer = SDJWTIssuerACAPy( + user_claims=payload, + issuer_key=None, + holder_key=None, + profile=profile, + did=did, + verification_method=verification_method, + ) + + print("sd_jwt_payload: ", json.dumps(sd_jwt_issuer.sd_jwt_payload, indent=4)) + print("") + print("sd_jwt_issuer.ii_disclosures: ") + for sd in sd_jwt_issuer.ii_disclosures: + print(sd.key, sd.value) + print("") + print("sd_jwt_issuer.sd_jwt_issuance: ", sd_jwt_issuer.sd_jwt_issuance) + + return sd_jwt_issuer.sd_jwt_issuance diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..e12e200aa8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,33 @@ +aiohttp~=3.8.1 +aiohttp-apispec~=2.2.1 +aiohttp-cors~=0.7.0 +aioredis~=2.0.0 +apispec~=3.3.0 +async-timeout~=4.0.2 +base58~=2.1.0 +ConfigArgParse~=1.5.3 +deepmerge~=0.3.0 +ecdsa~=0.16.1 +jsonpath_ng==1.5.2 +Markdown~=3.1.1 +markupsafe==2.0.1 +marshmallow~=3.20.1 +msgpack~=1.0 +multiformats~=0.2.1 +nest_asyncio~=1.5.5 +packaging~=23.1 +portalocker~=2.7.0 +prompt_toolkit~=2.0.9 +pydid~=0.3.6 +pyjwt~=2.8.0 +pyld~=2.0.3 +pynacl~=1.5.0 +python-dateutil~=2.8.1 +python-json-logger~=2.0.7 +pytz~=2021.1 +pyyaml~=6.0.1 +qrcode[pil]~=6.1 +requests~=2.31.0 +rlp==1.2.0 +unflatten~=0.1 +git+https://github.com/danielfett/sd-jwt.git@main From 469b931351e8260f71be5d3c6c8b423be6f070de Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 14 Aug 2023 17:26:05 -0600 Subject: [PATCH 03/68] test: (WIP) sd-jwt sign Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 1 + aries_cloudagent/wallet/tests/test_sd_jwt.py | 77 ++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 aries_cloudagent/wallet/tests/test_sd_jwt.py diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index d35a1bd25d..8722ce0e0c 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -88,6 +88,7 @@ async def sd_jwt_sign( issuer_key=None, holder_key=None, profile=profile, + headers=headers, did=did, verification_method=verification_method, ) diff --git a/aries_cloudagent/wallet/tests/test_sd_jwt.py b/aries_cloudagent/wallet/tests/test_sd_jwt.py new file mode 100644 index 0000000000..5fe21516b3 --- /dev/null +++ b/aries_cloudagent/wallet/tests/test_sd_jwt.py @@ -0,0 +1,77 @@ +from unittest import TestCase +import pytest +from aries_cloudagent.resolver.did_resolver import DIDResolver +from aries_cloudagent.resolver.tests.test_did_resolver import MockResolver +from aries_cloudagent.wallet.default_verification_key_strategy import ( + BaseVerificationKeyStrategy, + DefaultVerificationKeyStrategy, +) +from jsonpath_ng.ext import parse +import json + + +from ...core.in_memory.profile import InMemoryProfile +from ...wallet.did_method import KEY, DIDMethods +from ...wallet.key_type import ED25519 +from ...wallet.in_memory import InMemoryWallet + +from ..jwt import jwt_sign, jwt_verify, resolve_public_key_by_kid_for_verify +from ..sd_jwt import sd_jwt_sign + +from .test_jwt import profile, in_memory_wallet + + +class TestSDJWT: + """Tests for JWT sign and verify using dids.""" + + seed = "testseed000000000000000000000001" + + @pytest.mark.asyncio + async def test_sign_with_did_key_and_verify(self, profile, in_memory_wallet): + did_info = await in_memory_wallet.create_local_did(KEY, ED25519, self.seed) + did = did_info.did + verification_method = None + sd_list = [ + "address", + "address.street_address", + "address.street_address.house_number", + "address.locality", + "address.region", + "address.country", + "given_name", + "family_name", + "email", + "phone_number", + "phone_number_verified", + "birthdate", + "updated_at", + "nationalities[1:3]", + ] + headers = {} + payload = { + "sub": "user_42", + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "phone_number_verified": True, + "address": { + "street_address": { + "house_number": "123", + "street": "Main St", + }, + "locality": "Anytown", + "region": "Anystate", + "country": "US", + }, + "birthdate": "1940-01-01", + "updated_at": 1570000000, + "nationalities": ["US", "DE", "SA"], + } + signed = await sd_jwt_sign( + profile, headers, payload, sd_list, did, verification_method + ) + + assert signed + + # assert await jwt_verify(profile, signed) From e8ec8c1ed99e59c559a256ea673ea6fbcc8c1da8 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Wed, 16 Aug 2023 10:02:07 -0600 Subject: [PATCH 04/68] fix: super().__init__() Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 8722ce0e0c..58838f2349 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -18,7 +18,7 @@ class SDJWTError(BaseError): class SDJWTIssuerACAPy(SDJWTIssuer): def __init__( self, - user_claims, + user_claims: dict, issuer_key, holder_key, profile: Profile, @@ -26,11 +26,11 @@ def __init__( did: Optional[str] = None, verification_method: Optional[str] = None, ): - super().__init__(user_claims, issuer_key, holder_key) self.profile = profile self.headers = headers self.did = did self.verification_method = verification_method + super().__init__(user_claims, issuer_key, holder_key) def _create_signed_jws(self): self.serialized_sd_jwt = "" @@ -61,7 +61,7 @@ async def sd_jwt_sign( sd_list: List, did: Optional[str] = None, verification_method: Optional[str] = None, -) -> str: +) -> SDJWTIssuerACAPy: sorted_sd_list = sort_sd_list(sd_list) for sd in sorted_sd_list: @@ -92,13 +92,6 @@ async def sd_jwt_sign( did=did, verification_method=verification_method, ) - - print("sd_jwt_payload: ", json.dumps(sd_jwt_issuer.sd_jwt_payload, indent=4)) - print("") - print("sd_jwt_issuer.ii_disclosures: ") - for sd in sd_jwt_issuer.ii_disclosures: - print(sd.key, sd.value) - print("") - print("sd_jwt_issuer.sd_jwt_issuance: ", sd_jwt_issuer.sd_jwt_issuance) + print(json.dumps(sd_jwt_issuer.sd_jwt_payload, indent=4)) return sd_jwt_issuer.sd_jwt_issuance From a1496fb65461c5fc425abcae90d6c6eff368586d Mon Sep 17 00:00:00 2001 From: Char Howland Date: Wed, 16 Aug 2023 11:59:41 -0600 Subject: [PATCH 05/68] fix: await jwt_sign() call Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 58838f2349..0986e57998 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -25,17 +25,25 @@ def __init__( headers: dict, did: Optional[str] = None, verification_method: Optional[str] = None, + add_decoy_claims: bool = False, + serialization_format: str = "compact", ): + self._user_claims = user_claims + self._issuer_key = issuer_key + self._holder_key = holder_key + self.profile = profile self.headers = headers self.did = did self.verification_method = verification_method - super().__init__(user_claims, issuer_key, holder_key) - def _create_signed_jws(self): - self.serialized_sd_jwt = "" + self._add_decoy_claims = add_decoy_claims + self._serialization_format = serialization_format + self.ii_disclosures = [] - return jwt_sign( + async def _create_signed_jws(self): + self.serialized_sd_jwt = "" + return await jwt_sign( self.profile, self.headers, self.sd_jwt_payload, @@ -43,6 +51,12 @@ def _create_signed_jws(self): self.verification_method, ) + async def issue(self): + self._check_for_sd_claim(self._user_claims) + self._assemble_sd_jwt_payload() + await self._create_signed_jws() + self._create_combined() + def sort_sd_list(sd_list): """ @@ -92,6 +106,7 @@ async def sd_jwt_sign( did=did, verification_method=verification_method, ) + await sd_jwt_issuer.issue() print(json.dumps(sd_jwt_issuer.sd_jwt_payload, indent=4)) return sd_jwt_issuer.sd_jwt_issuance From 471a819dc156de7b0c8e8869536a0c25a3e9721b Mon Sep 17 00:00:00 2001 From: Char Howland Date: Wed, 16 Aug 2023 12:23:13 -0600 Subject: [PATCH 06/68] feat: create SDJWSCreateSchema Signed-off-by: Char Howland --- aries_cloudagent/wallet/routes.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 66711ab8e5..e7e049fadd 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -172,6 +172,12 @@ class JWSCreateSchema(OpenAPISchema): ) +class SDJWSCreateSchema(JWSCreateSchema): + """Request schema to create an sd-jws with a particular DID.""" + + sd_list = fields.List() + + class JWSVerifySchema(OpenAPISchema): """Request schema to verify a jws created from a DID.""" @@ -945,7 +951,7 @@ async def wallet_jwt_sign(request: web.BaseRequest): @docs( tags=["wallet"], summary="Create a EdDSA sd-jws using did keys with a given payload" ) -@request_schema(JWSCreateSchema) # check +@request_schema(SDJWSCreateSchema) @response_schema(WalletModuleResponseSchema(), description="") async def wallet_sd_jwt_sign(request: web.BaseRequest): """ @@ -964,10 +970,11 @@ async def wallet_sd_jwt_sign(request: web.BaseRequest): verification_method = body.get("verificationMethod") headers = body.get("headers", {}) payload = body.get("payload", {}) + sd_list = body.get("sd)list", []) try: sd_jws = await sd_jwt_sign( - context.profile, headers, payload, did, verification_method + context.profile, headers, payload, sd_list, did, verification_method ) except ValueError as err: raise web.HTTPBadRequest(reason="Bad did or verification method") from err From 62dfabec5dad9f1ea3eeee3ffe928ebf213ce249 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Wed, 16 Aug 2023 13:10:11 -0600 Subject: [PATCH 07/68] fix: marshmallow error Signed-off-by: Char Howland --- aries_cloudagent/wallet/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index e7e049fadd..754cf3a664 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -175,7 +175,7 @@ class JWSCreateSchema(OpenAPISchema): class SDJWSCreateSchema(JWSCreateSchema): """Request schema to create an sd-jws with a particular DID.""" - sd_list = fields.List() + sd_list = fields.List(fields.Str) class JWSVerifySchema(OpenAPISchema): From d4b36e77e2c686738d59ed59c8a2366e059aec99 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Wed, 16 Aug 2023 13:48:33 -0600 Subject: [PATCH 08/68] fix: flake8 Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 0986e57998..d10ae3f9bd 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -12,10 +12,12 @@ class SDJWTError(BaseError): - """SD-JWT Error""" + """SD-JWT Error.""" class SDJWTIssuerACAPy(SDJWTIssuer): + """SDJWTIssuer class for ACA-Py implementation.""" + def __init__( self, user_claims: dict, @@ -28,6 +30,7 @@ def __init__( add_decoy_claims: bool = False, serialization_format: str = "compact", ): + """Initalize an SDJWTIssuerACAPy instance.""" self._user_claims = user_claims self._issuer_key = issuer_key self._holder_key = holder_key @@ -52,6 +55,7 @@ async def _create_signed_jws(self): ) async def issue(self): + """Issue an sd-jwt.""" self._check_for_sd_claim(self._user_claims) self._assemble_sd_jwt_payload() await self._create_signed_jws() @@ -60,7 +64,9 @@ async def issue(self): def sort_sd_list(sd_list): """ - Sorts sd list such that selectively disclosable claims deepest + Sorts sd_list. + + Ensures that selectively disclosable claims deepest in the structure are handled first. """ nested_claim_sort = [(len(sd.split(".")), sd) for sd in sd_list] @@ -75,7 +81,14 @@ async def sd_jwt_sign( sd_list: List, did: Optional[str] = None, verification_method: Optional[str] = None, -) -> SDJWTIssuerACAPy: +) -> str: + """ + Sign sd-jwt. + + Use sd_list to wrap selectively disclosable claims with + SDObj within payload, create SDJWTIssuerACAPy object, and + call SDJWTIssuerACAPy.issue(). + """ sorted_sd_list = sort_sd_list(sd_list) for sd in sorted_sd_list: From 1615b4fe188948089997b6cfd34c32edd0288c02 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Wed, 16 Aug 2023 16:44:14 -0600 Subject: [PATCH 09/68] fix: typo Signed-off-by: Char Howland --- aries_cloudagent/wallet/routes.py | 2 +- aries_cloudagent/wallet/sd_jwt.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 754cf3a664..3e0614c7c8 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -175,7 +175,7 @@ class JWSCreateSchema(OpenAPISchema): class SDJWSCreateSchema(JWSCreateSchema): """Request schema to create an sd-jws with a particular DID.""" - sd_list = fields.List(fields.Str) + sd_list = fields.List(fields.Str()) class JWSVerifySchema(OpenAPISchema): diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index d10ae3f9bd..2005611c1d 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -30,7 +30,7 @@ def __init__( add_decoy_claims: bool = False, serialization_format: str = "compact", ): - """Initalize an SDJWTIssuerACAPy instance.""" + """Initialize an SDJWTIssuerACAPy instance.""" self._user_claims = user_claims self._issuer_key = issuer_key self._holder_key = holder_key From 14cee83824e02784990417b758584d44b49ad38d Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 17 Aug 2023 13:24:15 -0600 Subject: [PATCH 10/68] fix: typo Signed-off-by: Char Howland --- aries_cloudagent/wallet/jwt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/wallet/jwt.py b/aries_cloudagent/wallet/jwt.py index e66049a30a..fa9580e856 100644 --- a/aries_cloudagent/wallet/jwt.py +++ b/aries_cloudagent/wallet/jwt.py @@ -120,7 +120,7 @@ async def resolve_public_key_by_kid_for_verify(profile: Profile, kid: str) -> st async def jwt_verify(profile: Profile, jwt: str) -> JWTVerifyResult: """Verify a JWT and return the headers and payload.""" - encoded_headers, encoded_payload, encoded_signiture = jwt.split(".", 3) + encoded_headers, encoded_payload, encoded_signature = jwt.split(".", 3) headers = b64_to_dict(encoded_headers) if "alg" not in headers or headers["alg"] != "EdDSA" or "kid" not in headers: raise BadJWSHeaderError( @@ -129,7 +129,7 @@ async def jwt_verify(profile: Profile, jwt: str) -> JWTVerifyResult: payload = b64_to_dict(encoded_payload) verification_method = headers["kid"] - decoded_signature = b64_to_bytes(encoded_signiture, urlsafe=True) + decoded_signature = b64_to_bytes(encoded_signature, urlsafe=True) async with profile.session() as session: verkey = await resolve_public_key_by_kid_for_verify( From 04086df35a8c970f176e7db0666b8f33986ac324 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 17 Aug 2023 13:26:20 -0600 Subject: [PATCH 11/68] fix: set jwt_sign result on self.serialized_sd_jwt Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 2005611c1d..ae6b19df9f 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -45,8 +45,7 @@ def __init__( self.ii_disclosures = [] async def _create_signed_jws(self): - self.serialized_sd_jwt = "" - return await jwt_sign( + self.serialized_sd_jwt = await jwt_sign( self.profile, self.headers, self.sd_jwt_payload, From cc36294d05b600a7b29cdbc0db8800c12ae0312d Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 17 Aug 2023 13:30:32 -0600 Subject: [PATCH 12/68] feat: create endpoint for sd-jwt verify Signed-off-by: Char Howland --- aries_cloudagent/wallet/routes.py | 33 ++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 3e0614c7c8..693077d3d4 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -50,7 +50,7 @@ from ..resolver.base import ResolverError from ..storage.error import StorageError, StorageNotFoundError from ..wallet.jwt import jwt_sign, jwt_verify -from ..wallet.sd_jwt import sd_jwt_sign +from ..wallet.sd_jwt import sd_jwt_sign, sd_jwt_verify from .base import BaseWallet from .did_info import DIDInfo from .did_method import KEY, SOV, DIDMethod, DIDMethods, HolderDefinedDid @@ -1015,6 +1015,36 @@ async def wallet_jwt_verify(request: web.BaseRequest): ) +@docs(tags=["wallet"], summary="Verify a EdDSA sd-jws using did keys with a given JWS") +@request_schema(JWSVerifySchema()) +@response_schema(JWSVerifyResponseSchema(), 200, description="") +async def wallet_sd_jwt_verify(request: web.BaseRequest): + """ + Request handler for sd-jws validation using did. + + Args: + "sd-jwt": { ... } + """ + context: AdminRequestContext = request["context"] + body = await request.json() + sd_jwt = body["sd_jwt"] + try: + result = await sd_jwt_verify(context.profile, sd_jwt) + except (BadJWSHeaderError, InvalidVerificationMethod) as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + except ResolverError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + + return web.json_response( + { + "valid": result.valid, + "headers": result.headers, + "payload": result.payload, + "kid": result.kid, + } + ) + + @docs(tags=["wallet"], summary="Query DID endpoint in wallet") @querystring_schema(DIDQueryStringSchema()) @response_schema(DIDEndpointSchema, 200, description="") @@ -1171,6 +1201,7 @@ async def register(app: web.Application): web.post("/wallet/jwt/sign", wallet_jwt_sign), web.post("/wallet/jwt/verify", wallet_jwt_verify), web.post("/wallet/sd-jwt/sign", wallet_sd_jwt_sign), + web.post("/wallet/sd-jwt/verify", wallet_sd_jwt_verify), web.get( "/wallet/get-did-endpoint", wallet_get_did_endpoint, allow_head=False ), From 03f7c44bfe2b8a5510e30bb27cf9b591157ff2a2 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 17 Aug 2023 13:32:15 -0600 Subject: [PATCH 13/68] feat: create SDJWTVerifierACAPy class and sd_jwt_verify() method Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 43 ++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index ae6b19df9f..9cbde95e44 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -5,9 +5,10 @@ from jsonpath_ng.ext import parse from sd_jwt.common import SDObj from sd_jwt.issuer import SDJWTIssuer +from sd_jwt.verifier import SDJWTVerifier from ..core.profile import Profile -from ..wallet.jwt import jwt_sign +from ..wallet.jwt import JWTVerifyResult, jwt_sign, jwt_verify from ..core.error import BaseError @@ -122,3 +123,43 @@ async def sd_jwt_sign( print(json.dumps(sd_jwt_issuer.sd_jwt_payload, indent=4)) return sd_jwt_issuer.sd_jwt_issuance + + +class SDJWTVerifierACAPy(SDJWTVerifier): + def __init__( + self, + profile: Profile, + sd_jwt_presentation: str, + serialization_format: str = "compact", + ): + self.profile = profile + self.sd_jwt_presentation = sd_jwt_presentation + self.jwt = "" + self._serialization_format = serialization_format + + async def _verify_sd_jwt(self): + self.serialized_sd_jwt = "" + return await jwt_verify( + self.profile, + self.jwt, + ) + + def _parse_sd_jwt(self, sd_jwt): + if self._serialization_format == "compact": + ( + self._unverified_input_sd_jwt, + *self._input_disclosures, + self._unverified_input_key_binding_jwt, + ) = self._split(sd_jwt) + return self._unverified_input_sd_jwt + + async def verify(self): + self.jwt = self._parse_sd_jwt(self.sd_jwt_presentation) + self._create_hash_mappings(self._input_disclosures) + return await self._verify_sd_jwt() + + +async def sd_jwt_verify(profile: Profile, sd_jwt_presentation: str) -> JWTVerifyResult: + sd_jwt_verifier = SDJWTVerifierACAPy(profile, sd_jwt_presentation) + verified = await sd_jwt_verifier.verify() + return verified.valid From cea0f152dcc02ab7270585a9708393dcc73ddf41 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 17 Aug 2023 13:32:53 -0600 Subject: [PATCH 14/68] test: sd_jwt_verify() Signed-off-by: Char Howland --- aries_cloudagent/wallet/tests/test_sd_jwt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/wallet/tests/test_sd_jwt.py b/aries_cloudagent/wallet/tests/test_sd_jwt.py index 5fe21516b3..64e4f858b2 100644 --- a/aries_cloudagent/wallet/tests/test_sd_jwt.py +++ b/aries_cloudagent/wallet/tests/test_sd_jwt.py @@ -16,7 +16,7 @@ from ...wallet.in_memory import InMemoryWallet from ..jwt import jwt_sign, jwt_verify, resolve_public_key_by_kid_for_verify -from ..sd_jwt import sd_jwt_sign +from ..sd_jwt import sd_jwt_sign, sd_jwt_verify from .test_jwt import profile, in_memory_wallet @@ -74,4 +74,4 @@ async def test_sign_with_did_key_and_verify(self, profile, in_memory_wallet): assert signed - # assert await jwt_verify(profile, signed) + assert await sd_jwt_verify(profile, signed) From 0e013995d429668e9d6b34689a7eeefd5daa8818 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 17 Aug 2023 13:40:56 -0600 Subject: [PATCH 15/68] fix: remove self.jwt Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 9cbde95e44..3bbe4ed6e5 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -134,14 +134,12 @@ def __init__( ): self.profile = profile self.sd_jwt_presentation = sd_jwt_presentation - self.jwt = "" self._serialization_format = serialization_format async def _verify_sd_jwt(self): - self.serialized_sd_jwt = "" return await jwt_verify( self.profile, - self.jwt, + self.serialized_sd_jwt, ) def _parse_sd_jwt(self, sd_jwt): @@ -154,7 +152,7 @@ def _parse_sd_jwt(self, sd_jwt): return self._unverified_input_sd_jwt async def verify(self): - self.jwt = self._parse_sd_jwt(self.sd_jwt_presentation) + self.serialized_sd_jwt = self._parse_sd_jwt(self.sd_jwt_presentation) self._create_hash_mappings(self._input_disclosures) return await self._verify_sd_jwt() From d06286bde930f7706afd72d13acac157aee114d8 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 17 Aug 2023 16:49:10 -0600 Subject: [PATCH 16/68] feat: create SDJWSVerifyResponseSchema Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 95 +++++++++++++++++++++++++++++ aries_cloudagent/wallet/routes.py | 40 ++++++++++-- 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 73e66af0d0..ef08fcdd66 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -200,6 +200,29 @@ def __init__(self): ) +class SDList(Regexp): + """Validate SD List""" + + EXAMPLE = [ + "given_name", + "family_name", + "address", + "address.street_address", + "address.street_address.house_number", + "email", + "nationalities[1:3]", + ] + PATTERN = r".*" # TODO: add Regex + + def __init__(self): + """Initialize the instance.""" + + super().__init__( + SDList.PATTERN, + error="Value {input} is not a valid SDList", + ) + + class JSONWebToken(Regexp): """Validate JSON Web Token.""" @@ -219,6 +242,69 @@ def __init__(self): ) +class SDJSONWebToken(Regexp): + """Validate SD-JSON Web Token.""" + + EXAMPLE = ( + "eyJhbGciOiJFZERTQSJ9." + "eyJhIjogIjAifQ." + "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" + "~WyJEM3BUSFdCYWNRcFdpREc2TWZKLUZnIiwgIkRFIl0" + "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" + "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" + ) + PATTERN = r".*" # TODO: add Regex + + def __init__(self): + """Initialize the instance.""" + + super().__init__( + SDJSONWebToken.PATTERN, + error="Value {input} is not a valid SD-JSON Web token", + ) + + +class Disclosures(Regexp): + """Validate SD Disclosures.""" + + EXAMPLE = [ + ["uR_7gCub2H8jqCebK974gg", "given_name", "John"], + ["pVNC8_UIAFubHCE8qd8Qag", "family_name", "Doe"], + [ + "wn7kQK6gc1xjK2EUvdBx_w", + "address", + { + "_sd": [ + "62Z6rlJFKNagx_zCgTKdEKVGx5oeCGmFOBYP_V3nvXM", + "BJfn94_yMmEe3SsqdWwaPtWL0K92Q2AEEOKCIW0_ijI", + "W5Pb28JILuWXn3MzMybIREwEMo92ho07zAO_Zfp6UW4", + "XNseAXkWgm9P5cc7D5RPu3mA8uYuFWtK9vxwTQQZVW8", + ] + }, + ], + [ + "QHp6MA7HoUE3Ajan3LCqqw", + "street_address", + { + "_sd": ["5sYxNNvht8a9g5JON9yQWf4eIyogMtV7_7Y1VWh9QGU"], + "street": "Main St", + }, + ], + ["pFlIYzaDRUqagcTPlHD1-Q", "house_number", "123"], + ["pvyM3ymTZWjA4N6z_u7ufA", "email", "johndoe@example.com"], + ["P4vbBZ2_7t12LF1DKsSSMQ", "DE"], + ["jljUb3RcAszB__K8e0WP-Q", "SA"], + ] + PATTERN = r".*" # TODO: add Regex + + def __init__(self): + """Initialize the instance.""" + + super().__init__( + Disclosures.PATTERN, error="Value {input} is not a valid disclosure" + ) + + class DIDKey(Regexp): """Validate value against DID key specification.""" @@ -800,9 +886,18 @@ def __init__( JWS_HEADER_KID_VALIDATE = JWSHeaderKid() JWS_HEADER_KID_EXAMPLE = JWSHeaderKid.EXAMPLE +# SD_LIST_VALIDATE = SDList() +# SD_LIST_EXAMPLE = SDList().EXAMPLE + JWT_VALIDATE = JSONWebToken() JWT_EXAMPLE = JSONWebToken.EXAMPLE +SD_JWT_VALIDATE = SDJSONWebToken() +SD_JWT_EXAMPLE = SDJSONWebToken.EXAMPLE + +DISCLOSURES_VALIDATE = Disclosures() +DISCLOSURES_EXAMPLE = Disclosures().EXAMPLE + DID_KEY_VALIDATE = DIDKey() DID_KEY_EXAMPLE = DIDKey.EXAMPLE diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 693077d3d4..5319984bff 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -23,6 +23,8 @@ from ..messaging.valid import ( DID_POSTURE_EXAMPLE, DID_POSTURE_VALIDATE, + DISCLOSURES_EXAMPLE, + DISCLOSURES_VALIDATE, ENDPOINT_EXAMPLE, ENDPOINT_TYPE_EXAMPLE, ENDPOINT_TYPE_VALIDATE, @@ -35,6 +37,10 @@ INDY_RAW_PUBLIC_KEY_VALIDATE, JWT_EXAMPLE, JWT_VALIDATE, + SD_JWT_EXAMPLE, + SD_JWT_VALIDATE, + SD_LIST_EXAMPLE, + SD_LIST_VALIDATE, IndyDID, Uri, ) @@ -175,7 +181,13 @@ class JWSCreateSchema(OpenAPISchema): class SDJWSCreateSchema(JWSCreateSchema): """Request schema to create an sd-jws with a particular DID.""" - sd_list = fields.List(fields.Str()) + sd_list = fields.List( + fields.Str( + required=False, + validate=SD_LIST_VALIDATE, + metadata={"example": SD_LIST_EXAMPLE}, + ) + ) class JWSVerifySchema(OpenAPISchema): @@ -184,8 +196,14 @@ class JWSVerifySchema(OpenAPISchema): jwt = fields.Str(validate=JWT_VALIDATE, metadata={"example": JWT_EXAMPLE}) +class SDJWSVerifySchema(OpenAPISchema): + """Request schema to verify an sd-jws created from a DID.""" + + sd_jwt = fields.Str(validate=SD_JWT_VALIDATE, metadata={"example": SD_JWT_EXAMPLE}) + + class JWSVerifyResponseSchema(OpenAPISchema): - """Response schema for verification result.""" + """Response schema for JWT verification result.""" valid = fields.Bool(required=True) error = fields.Str(required=False, metadata={"description": "Error text"}) @@ -198,6 +216,20 @@ class JWSVerifyResponseSchema(OpenAPISchema): ) +class SDJWSVerifyResponseSchema(JWSVerifyResponseSchema): + """Response schema for SD-JWT verification result.""" + + disclosures = fields.List( + fields.List( + fields.Str( + required=False, + validate=DISCLOSURES_VALIDATE, + metadata={"example": DISCLOSURES_EXAMPLE}, + ) + ) + ) + + class DIDEndpointSchema(OpenAPISchema): """Request schema to set DID endpoint; response schema to get DID endpoint.""" @@ -1016,8 +1048,8 @@ async def wallet_jwt_verify(request: web.BaseRequest): @docs(tags=["wallet"], summary="Verify a EdDSA sd-jws using did keys with a given JWS") -@request_schema(JWSVerifySchema()) -@response_schema(JWSVerifyResponseSchema(), 200, description="") +@request_schema(SDJWSVerifySchema()) +@response_schema(SDJWSVerifyResponseSchema(), 200, description="") async def wallet_sd_jwt_verify(request: web.BaseRequest): """ Request handler for sd-jws validation using did. From f1f3fdf3e2c3cc5fdc6c69c4a21a9ac15eb8eb9b Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 17 Aug 2023 16:50:29 -0600 Subject: [PATCH 17/68] feat: create SDJWTVerifyResult Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 50 +++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 3bbe4ed6e5..fbef87d953 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -1,14 +1,14 @@ """Operations supporting SD-JWT creation and verification.""" import json -from typing import Any, List, Mapping, Optional +from typing import Any, List, Mapping, NamedTuple, Optional from jsonpath_ng.ext import parse from sd_jwt.common import SDObj from sd_jwt.issuer import SDJWTIssuer from sd_jwt.verifier import SDJWTVerifier from ..core.profile import Profile -from ..wallet.jwt import JWTVerifyResult, jwt_sign, jwt_verify +from ..wallet.jwt import jwt_sign, jwt_verify from ..core.error import BaseError @@ -125,22 +125,43 @@ async def sd_jwt_sign( return sd_jwt_issuer.sd_jwt_issuance +class SDJWTVerifyResult(NamedTuple): + """Result from verifying SD-JWT""" + + headers: Mapping[str, Any] + payload: Mapping[str, Any] + valid: bool + kid: str + disclosures: str + # TODO: figure out inheritance + + class SDJWTVerifierACAPy(SDJWTVerifier): + """SDJWTVerifier class for ACA-Py implementation.""" + def __init__( self, profile: Profile, sd_jwt_presentation: str, serialization_format: str = "compact", ): + """Initialize an SDJWTVerifierACAPy instance.""" self.profile = profile self.sd_jwt_presentation = sd_jwt_presentation self._serialization_format = serialization_format - async def _verify_sd_jwt(self): - return await jwt_verify( + async def _verify_sd_jwt(self) -> SDJWTVerifyResult: + verified = await jwt_verify( self.profile, self.serialized_sd_jwt, ) + return SDJWTVerifyResult( + headers=verified.headers, + payload=verified.payload, + valid=verified.valid, + kid=verified.kid, + disclosures=self._disclosures_list, + ) def _parse_sd_jwt(self, sd_jwt): if self._serialization_format == "compact": @@ -150,14 +171,31 @@ def _parse_sd_jwt(self, sd_jwt): self._unverified_input_key_binding_jwt, ) = self._split(sd_jwt) return self._unverified_input_sd_jwt + # TOOD: what to do about the else? + + def _create_disclosures_list(self) -> List: + disclosures_list = [] + for disclosure in self._input_disclosures: + disclosures_list.append( + json.loads(self._base64url_decode(disclosure).decode("utf-8")) + ) + + return disclosures_list async def verify(self): + """Verify an sd-jwt.""" self.serialized_sd_jwt = self._parse_sd_jwt(self.sd_jwt_presentation) self._create_hash_mappings(self._input_disclosures) + self._disclosures_list = self._create_disclosures_list() return await self._verify_sd_jwt() -async def sd_jwt_verify(profile: Profile, sd_jwt_presentation: str) -> JWTVerifyResult: +async def sd_jwt_verify( + profile: Profile, sd_jwt_presentation: str +) -> SDJWTVerifyResult: + """ + Verify sd-jwt using SDJWTVerifierACAPy.verify(). + """ sd_jwt_verifier = SDJWTVerifierACAPy(profile, sd_jwt_presentation) verified = await sd_jwt_verifier.verify() - return verified.valid + return verified From 9583624d8642f0d424b5955c51d1009f0adab6ea Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 21 Aug 2023 15:33:17 -0600 Subject: [PATCH 18/68] feat: sdlist regex (WIP) Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 11 ++++------- aries_cloudagent/wallet/routes.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index ef08fcdd66..86c79dfe45 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -204,15 +204,12 @@ class SDList(Regexp): """Validate SD List""" EXAMPLE = [ - "given_name", - "family_name", + "name", "address", "address.street_address", - "address.street_address.house_number", - "email", "nationalities[1:3]", ] - PATTERN = r".*" # TODO: add Regex + PATTERN = r"[a-z0-9:\[\]_\.@?\(\)]" # TODO: check def __init__(self): """Initialize the instance.""" @@ -886,8 +883,8 @@ def __init__( JWS_HEADER_KID_VALIDATE = JWSHeaderKid() JWS_HEADER_KID_EXAMPLE = JWSHeaderKid.EXAMPLE -# SD_LIST_VALIDATE = SDList() -# SD_LIST_EXAMPLE = SDList().EXAMPLE +SD_LIST_VALIDATE = SDList() +SD_LIST_EXAMPLE = SDList().EXAMPLE JWT_VALIDATE = JSONWebToken() JWT_EXAMPLE = JSONWebToken.EXAMPLE diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 5319984bff..d7ba16a999 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -1002,7 +1002,7 @@ async def wallet_sd_jwt_sign(request: web.BaseRequest): verification_method = body.get("verificationMethod") headers = body.get("headers", {}) payload = body.get("payload", {}) - sd_list = body.get("sd)list", []) + sd_list = body.get("sd_list", []) try: sd_jws = await sd_jwt_sign( From 212e9cae5ce0b64f130d1a5d76099c95aa08bde9 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 21 Aug 2023 15:34:17 -0600 Subject: [PATCH 19/68] feat: sd-jwt regex validation Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 86c79dfe45..a255724418 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -250,7 +250,7 @@ class SDJSONWebToken(Regexp): "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" ) - PATTERN = r".*" # TODO: add Regex + PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*([~Wy][a-zA-Z0-9]*)*$" def __init__(self): """Initialize the instance.""" From c87f1b8f93d69b6be0c3e82fe7cb79225e125dc9 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 21 Aug 2023 15:35:42 -0600 Subject: [PATCH 20/68] feat: remove disclosures validation Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 44 ----------------------------- aries_cloudagent/wallet/routes.py | 12 +------- 2 files changed, 1 insertion(+), 55 deletions(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index a255724418..7c7178a49c 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -261,47 +261,6 @@ def __init__(self): ) -class Disclosures(Regexp): - """Validate SD Disclosures.""" - - EXAMPLE = [ - ["uR_7gCub2H8jqCebK974gg", "given_name", "John"], - ["pVNC8_UIAFubHCE8qd8Qag", "family_name", "Doe"], - [ - "wn7kQK6gc1xjK2EUvdBx_w", - "address", - { - "_sd": [ - "62Z6rlJFKNagx_zCgTKdEKVGx5oeCGmFOBYP_V3nvXM", - "BJfn94_yMmEe3SsqdWwaPtWL0K92Q2AEEOKCIW0_ijI", - "W5Pb28JILuWXn3MzMybIREwEMo92ho07zAO_Zfp6UW4", - "XNseAXkWgm9P5cc7D5RPu3mA8uYuFWtK9vxwTQQZVW8", - ] - }, - ], - [ - "QHp6MA7HoUE3Ajan3LCqqw", - "street_address", - { - "_sd": ["5sYxNNvht8a9g5JON9yQWf4eIyogMtV7_7Y1VWh9QGU"], - "street": "Main St", - }, - ], - ["pFlIYzaDRUqagcTPlHD1-Q", "house_number", "123"], - ["pvyM3ymTZWjA4N6z_u7ufA", "email", "johndoe@example.com"], - ["P4vbBZ2_7t12LF1DKsSSMQ", "DE"], - ["jljUb3RcAszB__K8e0WP-Q", "SA"], - ] - PATTERN = r".*" # TODO: add Regex - - def __init__(self): - """Initialize the instance.""" - - super().__init__( - Disclosures.PATTERN, error="Value {input} is not a valid disclosure" - ) - - class DIDKey(Regexp): """Validate value against DID key specification.""" @@ -892,9 +851,6 @@ def __init__( SD_JWT_VALIDATE = SDJSONWebToken() SD_JWT_EXAMPLE = SDJSONWebToken.EXAMPLE -DISCLOSURES_VALIDATE = Disclosures() -DISCLOSURES_EXAMPLE = Disclosures().EXAMPLE - DID_KEY_VALIDATE = DIDKey() DID_KEY_EXAMPLE = DIDKey.EXAMPLE diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index d7ba16a999..458bbc55c9 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -23,8 +23,6 @@ from ..messaging.valid import ( DID_POSTURE_EXAMPLE, DID_POSTURE_VALIDATE, - DISCLOSURES_EXAMPLE, - DISCLOSURES_VALIDATE, ENDPOINT_EXAMPLE, ENDPOINT_TYPE_EXAMPLE, ENDPOINT_TYPE_VALIDATE, @@ -219,15 +217,7 @@ class JWSVerifyResponseSchema(OpenAPISchema): class SDJWSVerifyResponseSchema(JWSVerifyResponseSchema): """Response schema for SD-JWT verification result.""" - disclosures = fields.List( - fields.List( - fields.Str( - required=False, - validate=DISCLOSURES_VALIDATE, - metadata={"example": DISCLOSURES_EXAMPLE}, - ) - ) - ) + disclosures = fields.List(fields.List(fields.Str())) # TODO: check class DIDEndpointSchema(OpenAPISchema): From f7d5c8d317b0c10943bda019574920179487f09f Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 21 Aug 2023 15:36:59 -0600 Subject: [PATCH 21/68] fix: add disclosures to sd-jwt verify json response Signed-off-by: Char Howland --- aries_cloudagent/wallet/routes.py | 5 ++++- aries_cloudagent/wallet/sd_jwt.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 458bbc55c9..878ae39af5 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -1037,7 +1037,9 @@ async def wallet_jwt_verify(request: web.BaseRequest): ) -@docs(tags=["wallet"], summary="Verify a EdDSA sd-jws using did keys with a given JWS") +@docs( + tags=["wallet"], summary="Verify a EdDSA sd-jws using did keys with a given SD-JWS" +) @request_schema(SDJWSVerifySchema()) @response_schema(SDJWSVerifyResponseSchema(), 200, description="") async def wallet_sd_jwt_verify(request: web.BaseRequest): @@ -1063,6 +1065,7 @@ async def wallet_sd_jwt_verify(request: web.BaseRequest): "headers": result.headers, "payload": result.payload, "kid": result.kid, + "disclosures": result.disclosures, } ) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index fbef87d953..05c9ebc9cb 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -95,7 +95,7 @@ async def sd_jwt_sign( jsonpath_expression = parse(f"$.{sd}") matches = jsonpath_expression.find(payload) if len(matches) < 1: - raise SDJWTError("Claim for {sd} not found in payload.") + raise SDJWTError(f"Claim for {sd} not found in payload.") else: for match in matches: if type(match.context.value) is list: From 5daf780208a5d22e1f6fda9784d8012a8c52edff Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 24 Aug 2023 12:28:21 -0600 Subject: [PATCH 22/68] fix: validation for SDJWSVerifyResponseSchema Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 2 +- aries_cloudagent/wallet/routes.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 7c7178a49c..d1bc31c3c1 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -209,7 +209,7 @@ class SDList(Regexp): "address.street_address", "nationalities[1:3]", ] - PATTERN = r"[a-z0-9:\[\]_\.@?\(\)]" # TODO: check + PATTERN = r"[a-z0-9:\[\]_\.@?\(\)]" def __init__(self): """Initialize the instance.""" diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 878ae39af5..c53da9a31c 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -40,6 +40,7 @@ SD_LIST_EXAMPLE, SD_LIST_VALIDATE, IndyDID, + StrOrDictField, Uri, ) from ..protocols.coordinate_mediation.v1_0.route_manager import RouteManager @@ -217,7 +218,20 @@ class JWSVerifyResponseSchema(OpenAPISchema): class SDJWSVerifyResponseSchema(JWSVerifyResponseSchema): """Response schema for SD-JWT verification result.""" - disclosures = fields.List(fields.List(fields.Str())) # TODO: check + disclosures = fields.List( + fields.List(StrOrDictField()), + metadata={ + "description": "Disclosure arrays associated with the SD-JWT", + "example": [ + ["fx1iT_mETjGiC-JzRARnVg", "name", "Alice"], + [ + "n4-t3mlh8jSS6yMIT7QHnA", + "street_address", + {"_sd": ["kLZrLK7enwfqeOzJ9-Ss88YS3mhjOAEk9lr_ix2Heng"]}, + ], + ], + }, + ) class DIDEndpointSchema(OpenAPISchema): From a702859ffdfd91a687dbf757ae8b0182a79b4b0a Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 24 Aug 2023 12:29:51 -0600 Subject: [PATCH 23/68] fix: use inheritance for SDJWTVerifyResult Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 05c9ebc9cb..aaee7cb7e9 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -1,14 +1,14 @@ """Operations supporting SD-JWT creation and verification.""" import json -from typing import Any, List, Mapping, NamedTuple, Optional +from typing import Any, List, Mapping, Optional from jsonpath_ng.ext import parse from sd_jwt.common import SDObj from sd_jwt.issuer import SDJWTIssuer from sd_jwt.verifier import SDJWTVerifier from ..core.profile import Profile -from ..wallet.jwt import jwt_sign, jwt_verify +from ..wallet.jwt import JWTVerifyResult, jwt_sign, jwt_verify from ..core.error import BaseError @@ -120,20 +120,17 @@ async def sd_jwt_sign( verification_method=verification_method, ) await sd_jwt_issuer.issue() - print(json.dumps(sd_jwt_issuer.sd_jwt_payload, indent=4)) return sd_jwt_issuer.sd_jwt_issuance -class SDJWTVerifyResult(NamedTuple): +class SDJWTVerifyResult(JWTVerifyResult): """Result from verifying SD-JWT""" - headers: Mapping[str, Any] - payload: Mapping[str, Any] - valid: bool - kid: str - disclosures: str - # TODO: figure out inheritance + disclosures: list + + def add_disclosures(self, disclosures): + self.disclosures = disclosures class SDJWTVerifierACAPy(SDJWTVerifier): @@ -155,13 +152,14 @@ async def _verify_sd_jwt(self) -> SDJWTVerifyResult: self.profile, self.serialized_sd_jwt, ) - return SDJWTVerifyResult( + sd_jwt_verify_result = SDJWTVerifyResult( headers=verified.headers, payload=verified.payload, valid=verified.valid, kid=verified.kid, - disclosures=self._disclosures_list, ) + sd_jwt_verify_result.add_disclosures(self._disclosures_list) + return sd_jwt_verify_result def _parse_sd_jwt(self, sd_jwt): if self._serialization_format == "compact": From 2f6f49d86a1b9b0dd460ea2c34f9127c614c3e7a Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 24 Aug 2023 12:30:29 -0600 Subject: [PATCH 24/68] fix: remove unnecessary type checking Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index aaee7cb7e9..b284ecc44f 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -101,14 +101,10 @@ async def sd_jwt_sign( if type(match.context.value) is list: match.context.value.remove(match.value) match.context.value.append(SDObj(match.value)) - elif type(match.context.value) is str or int or dict or bool: + else: match.context.value[ SDObj(str(match.path)) ] = match.context.value.pop(str(match.path)) - else: - raise SDJWTError( - f"Unrecognized type {type(match.context.value)} for {match.path}" - ) sd_jwt_issuer = SDJWTIssuerACAPy( user_claims=payload, From 66558a42a9976584f10342206d26a2a80ae6e46d Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 24 Aug 2023 12:31:01 -0600 Subject: [PATCH 25/68] feat: add else for JWS JSON serialization Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index b284ecc44f..3a5a2ac4ff 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -164,8 +164,18 @@ def _parse_sd_jwt(self, sd_jwt): *self._input_disclosures, self._unverified_input_key_binding_jwt, ) = self._split(sd_jwt) - return self._unverified_input_sd_jwt - # TOOD: what to do about the else? + else: + # if the SD-JWT is in JSON format, parse the json and extract the disclosures. + self._unverified_input_sd_jwt = sd_jwt + self._unverified_input_sd_jwt_parsed = json.loads(sd_jwt) + self._input_disclosures = self._unverified_input_sd_jwt_parsed[ + self.JWS_KEY_DISCLOSURES + ] + self._unverified_input_key_binding_jwt = ( + self._unverified_input_sd_jwt_parsed.get(self.JWS_KEY_KB_JWT, "") + ) + + return self._unverified_input_sd_jwt def _create_disclosures_list(self) -> List: disclosures_list = [] From 30c6e5c1957343fe96ed1f53ab5496833e31fb34 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 24 Aug 2023 12:34:40 -0600 Subject: [PATCH 26/68] test: fixture for payload Signed-off-by: Char Howland --- aries_cloudagent/wallet/tests/test_sd_jwt.py | 67 ++++++++++---------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/aries_cloudagent/wallet/tests/test_sd_jwt.py b/aries_cloudagent/wallet/tests/test_sd_jwt.py index 64e4f858b2..550984e371 100644 --- a/aries_cloudagent/wallet/tests/test_sd_jwt.py +++ b/aries_cloudagent/wallet/tests/test_sd_jwt.py @@ -1,53 +1,35 @@ -from unittest import TestCase import pytest -from aries_cloudagent.resolver.did_resolver import DIDResolver -from aries_cloudagent.resolver.tests.test_did_resolver import MockResolver -from aries_cloudagent.wallet.default_verification_key_strategy import ( - BaseVerificationKeyStrategy, - DefaultVerificationKeyStrategy, -) -from jsonpath_ng.ext import parse -import json - -from ...core.in_memory.profile import InMemoryProfile -from ...wallet.did_method import KEY, DIDMethods +from ...wallet.did_method import KEY from ...wallet.key_type import ED25519 -from ...wallet.in_memory import InMemoryWallet -from ..jwt import jwt_sign, jwt_verify, resolve_public_key_by_kid_for_verify -from ..sd_jwt import sd_jwt_sign, sd_jwt_verify +from ..sd_jwt import SDJWTVerifyResult, sd_jwt_sign, sd_jwt_verify from .test_jwt import profile, in_memory_wallet +@pytest.fixture +def create_address_payload(): + return { + "address": { + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US", + } + } + + class TestSDJWT: """Tests for JWT sign and verify using dids.""" seed = "testseed000000000000000000000001" + headers = {} @pytest.mark.asyncio async def test_sign_with_did_key_and_verify(self, profile, in_memory_wallet): did_info = await in_memory_wallet.create_local_did(KEY, ED25519, self.seed) - did = did_info.did verification_method = None - sd_list = [ - "address", - "address.street_address", - "address.street_address.house_number", - "address.locality", - "address.region", - "address.country", - "given_name", - "family_name", - "email", - "phone_number", - "phone_number_verified", - "birthdate", - "updated_at", - "nationalities[1:3]", - ] - headers = {} payload = { "sub": "user_42", "given_name": "John", @@ -68,10 +50,25 @@ async def test_sign_with_did_key_and_verify(self, profile, in_memory_wallet): "updated_at": 1570000000, "nationalities": ["US", "DE", "SA"], } + sd_list = [ + "address", + "address.street_address", + "address.street_address.house_number", + "address.locality", + "address.region", + "address.country", + "given_name", + "family_name", + "email", + "phone_number", + "phone_number_verified", + "birthdate", + "updated_at", + "nationalities[1:3]", + ] signed = await sd_jwt_sign( - profile, headers, payload, sd_list, did, verification_method + profile, self.headers, payload, sd_list, did_info.did, verification_method ) - assert signed assert await sd_jwt_verify(profile, signed) From 786d1078ecf1ea112f7ff81b9ae1154ac87e790e Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 24 Aug 2023 12:35:17 -0600 Subject: [PATCH 27/68] test: payloads with nested structures and array elements Signed-off-by: Char Howland --- aries_cloudagent/wallet/tests/test_sd_jwt.py | 120 +++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/aries_cloudagent/wallet/tests/test_sd_jwt.py b/aries_cloudagent/wallet/tests/test_sd_jwt.py index 550984e371..835365f388 100644 --- a/aries_cloudagent/wallet/tests/test_sd_jwt.py +++ b/aries_cloudagent/wallet/tests/test_sd_jwt.py @@ -72,3 +72,123 @@ async def test_sign_with_did_key_and_verify(self, profile, in_memory_wallet): assert signed assert await sd_jwt_verify(profile, signed) + + @pytest.mark.asyncio + async def test_flat_structure( + self, profile, in_memory_wallet, create_address_payload + ): + did_info = await in_memory_wallet.create_local_did(KEY, ED25519, self.seed) + verification_method = None + sd_list = ["address"] + signed = await sd_jwt_sign( + profile, + self.headers, + create_address_payload, + sd_list, + did_info.did, + verification_method, + ) + assert signed + + verified = await sd_jwt_verify(profile, signed) + assert isinstance(verified, SDJWTVerifyResult) + assert verified.valid + assert verified.payload["_sd"] + assert len(verified.payload["_sd"]) >= len(sd_list) + assert verified.payload["_sd_alg"] + for disclosure in verified.disclosures: + assert disclosure[1] in sd_list + + @pytest.mark.asyncio + async def test_nested_structure( + self, profile, in_memory_wallet, create_address_payload + ): + did_info = await in_memory_wallet.create_local_did(KEY, ED25519, self.seed) + verification_method = None + sd_list = [ + "address.street_address", + "address.locality", + "address.region", + "address.country", + ] + + signed = await sd_jwt_sign( + profile, + self.headers, + create_address_payload, + sd_list, + did_info.did, + verification_method, + ) + assert signed + + verified = await sd_jwt_verify(profile, signed) + assert isinstance(verified, SDJWTVerifyResult) + assert verified.valid + assert len(verified.payload["address"]["_sd"]) >= len(sd_list) + assert verified.payload["_sd_alg"] + for disclosure in verified.disclosures: + assert f"address.{disclosure[1]}" in sd_list + + @pytest.mark.asyncio + async def test_recursive_nested_structure( + self, profile, in_memory_wallet, create_address_payload + ): + did_info = await in_memory_wallet.create_local_did(KEY, ED25519, self.seed) + verification_method = None + sd_list = [ + "address", + "address.street_address", + "address.locality", + "address.region", + "address.country", + ] + + signed = await sd_jwt_sign( + profile, + self.headers, + create_address_payload, + sd_list, + did_info.did, + verification_method, + ) + assert signed + + verified = await sd_jwt_verify(profile, signed) + assert isinstance(verified, SDJWTVerifyResult) + assert verified.valid + assert "address" not in verified + assert verified.payload["_sd"] + assert verified.payload["_sd_alg"] + for disclosure in verified.disclosures: + if disclosure[1] == "address": + assert isinstance(disclosure[2], dict) + assert len(disclosure[2]["_sd"]) >= len(sd_list) - 1 + else: + assert f"address.{disclosure[1]}" in sd_list + + @pytest.mark.asyncio + async def test_list_splice(self, profile, in_memory_wallet): + did_info = await in_memory_wallet.create_local_did(KEY, ED25519, self.seed) + payload = {"nationalities": ["US", "DE", "SA"]} + verification_method = None + sd_list = ["nationalities[1:3]"] + + signed = await sd_jwt_sign( + profile, self.headers, payload, sd_list, did_info.did, verification_method + ) + assert signed + + verified = await sd_jwt_verify(profile, signed) + assert isinstance(verified, SDJWTVerifyResult) + assert verified.valid + for nationality in verified.payload["nationalities"]: + if isinstance(nationality, dict): + assert nationality["..."] + assert len(nationality) == 1 + else: + assert nationality in payload["nationalities"] + assert verified.payload["_sd_alg"] + spliced = [element.value for element in payload["nationalities"][1:3]] + for disclosure in verified.disclosures: + assert disclosure[1] in spliced From c0608baf32e8f8c2a8c8dd4904f670177d27049e Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 24 Aug 2023 15:04:35 -0600 Subject: [PATCH 28/68] feat: use BaseModel for JWTVerifyResult/SDJWTVerifyResult Signed-off-by: Char Howland --- aries_cloudagent/wallet/jwt.py | 45 ++++++++++++++--- aries_cloudagent/wallet/routes.py | 10 +--- aries_cloudagent/wallet/sd_jwt.py | 53 +++++++++++++++++--- aries_cloudagent/wallet/tests/test_sd_jwt.py | 2 +- 4 files changed, 88 insertions(+), 22 deletions(-) diff --git a/aries_cloudagent/wallet/jwt.py b/aries_cloudagent/wallet/jwt.py index fa9580e856..34d3eb65d5 100644 --- a/aries_cloudagent/wallet/jwt.py +++ b/aries_cloudagent/wallet/jwt.py @@ -2,13 +2,15 @@ import json import logging -from typing import Any, Mapping, NamedTuple, Optional +from typing import Any, Mapping, Optional +from marshmallow import fields from pydid import DIDUrl, Resource, VerificationMethod from ..core.profile import Profile from ..messaging.jsonld.error import BadJWSHeaderError, InvalidVerificationMethod from ..messaging.jsonld.routes import SUPPORTED_VERIFICATION_METHOD_TYPES +from ..messaging.models.base import BaseModel, BaseModelSchema from ..resolver.did_resolver import DIDResolver from .default_verification_key_strategy import BaseVerificationKeyStrategy from .base import BaseWallet @@ -88,13 +90,44 @@ async def jwt_sign( return f"{encoded_headers}.{encoded_payload}.{sig}" -class JWTVerifyResult(NamedTuple): +class JWTVerifyResult(BaseModel): """Result from verify.""" - headers: Mapping[str, Any] - payload: Mapping[str, Any] - valid: bool - kid: str + class Meta: + """JWTVerifyResult metadata.""" + + schema_class = "JWTVerifyResultSchema" + + def __init__( + self, + headers: Mapping[str, Any], + payload: Mapping[str, Any], + valid: bool, + kid: str, + ): + self.headers = headers + self.payload = payload + self.valid = valid + self.kid = kid + + +class JWTVerifyResultSchema(BaseModelSchema): + """JWTVerifyResult schema""" + + class Meta: + """JWTVerifyResultSchema metadata.""" + + model_class = JWTVerifyResult + + headers = fields.Dict( + required=True, metadata={"description": "Headers from verified JWT."} + ) + payload = fields.Dict( + required=True, metadata={"description": "Payload from verified JWT"} + ) + valid = fields.Bool(required=True) + kid = fields.Str(required=True, metadata={"description": "kid of signer"}) + error = fields.Str(required=False, metadata={"description": "Error text"}) async def resolve_public_key_by_kid_for_verify(profile: Profile, kid: str) -> str: diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index c53da9a31c..a8d1b81b6c 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -1073,15 +1073,7 @@ async def wallet_sd_jwt_verify(request: web.BaseRequest): except ResolverError as err: raise web.HTTPNotFound(reason=err.roll_up) from err - return web.json_response( - { - "valid": result.valid, - "headers": result.headers, - "payload": result.payload, - "kid": result.kid, - "disclosures": result.disclosures, - } - ) + return web.json_response(result.serialize()) @docs(tags=["wallet"], summary="Query DID endpoint in wallet") diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 3a5a2ac4ff..b265df849d 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -2,14 +2,16 @@ import json from typing import Any, List, Mapping, Optional +from marshmallow import fields from jsonpath_ng.ext import parse from sd_jwt.common import SDObj from sd_jwt.issuer import SDJWTIssuer from sd_jwt.verifier import SDJWTVerifier from ..core.profile import Profile -from ..wallet.jwt import JWTVerifyResult, jwt_sign, jwt_verify +from ..wallet.jwt import JWTVerifyResult, JWTVerifyResultSchema, jwt_sign, jwt_verify from ..core.error import BaseError +from ..messaging.valid import StrOrDictField class SDJWTError(BaseError): @@ -123,12 +125,52 @@ async def sd_jwt_sign( class SDJWTVerifyResult(JWTVerifyResult): """Result from verifying SD-JWT""" - disclosures: list + class Meta: + """SDJWTVerifyResult metadata.""" - def add_disclosures(self, disclosures): + schema_class = "SDJWTVerifyResultSchema" + + def __init__( + self, + headers, + payload, + valid, + kid, + disclosures, + ): + super().__init__( + headers, + payload, + valid, + kid, + ) self.disclosures = disclosures +class SDJWTVerifyResultSchema(JWTVerifyResultSchema): + """SDJWTVerifyResult schema""" + + class Meta: + """SDJWTVerifyResultSchema metadata.""" + + model_class = SDJWTVerifyResult + + disclosures = fields.List( + fields.List(StrOrDictField()), + metadata={ + "description": "Disclosure arrays associated with the SD-JWT", + "example": [ + ["fx1iT_mETjGiC-JzRARnVg", "name", "Alice"], + [ + "n4-t3mlh8jSS6yMIT7QHnA", + "street_address", + {"_sd": ["kLZrLK7enwfqeOzJ9-Ss88YS3mhjOAEk9lr_ix2Heng"]}, + ], + ], + }, + ) + + class SDJWTVerifierACAPy(SDJWTVerifier): """SDJWTVerifier class for ACA-Py implementation.""" @@ -148,14 +190,13 @@ async def _verify_sd_jwt(self) -> SDJWTVerifyResult: self.profile, self.serialized_sd_jwt, ) - sd_jwt_verify_result = SDJWTVerifyResult( + return SDJWTVerifyResult( headers=verified.headers, payload=verified.payload, valid=verified.valid, kid=verified.kid, + disclosures=self._disclosures_list, ) - sd_jwt_verify_result.add_disclosures(self._disclosures_list) - return sd_jwt_verify_result def _parse_sd_jwt(self, sd_jwt): if self._serialization_format == "compact": diff --git a/aries_cloudagent/wallet/tests/test_sd_jwt.py b/aries_cloudagent/wallet/tests/test_sd_jwt.py index 835365f388..81a7daa5b6 100644 --- a/aries_cloudagent/wallet/tests/test_sd_jwt.py +++ b/aries_cloudagent/wallet/tests/test_sd_jwt.py @@ -157,7 +157,7 @@ async def test_recursive_nested_structure( verified = await sd_jwt_verify(profile, signed) assert isinstance(verified, SDJWTVerifyResult) assert verified.valid - assert "address" not in verified + assert "address" not in verified.payload assert verified.payload["_sd"] assert verified.payload["_sd_alg"] for disclosure in verified.disclosures: From d41278810a8975e4c99afa72303a88e602247644 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 25 Aug 2023 09:35:29 -0600 Subject: [PATCH 29/68] fix: exponential backtracking issue Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index d1bc31c3c1..bdf1b26fd0 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -250,7 +250,7 @@ class SDJSONWebToken(Regexp): "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" ) - PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*([~Wy][a-zA-Z0-9]*)*$" + PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*(~Wy[a-zA-Z0-9]*)*$" def __init__(self): """Initialize the instance.""" From a46c4fe7c483872a5e0c41dc8ead2013bf7bf69c Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 25 Aug 2023 09:37:14 -0600 Subject: [PATCH 30/68] fix: regex for SDJSONWebToken Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index bdf1b26fd0..f51d83e596 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -250,7 +250,7 @@ class SDJSONWebToken(Regexp): "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" ) - PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*(~Wy[a-zA-Z0-9]*)*$" + PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*(~Wy[a-zA-Z0-9]+)*$" def __init__(self): """Initialize the instance.""" From 14a6df632b8b947271833bc14edc579ddbf5c312 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 25 Aug 2023 14:46:47 -0600 Subject: [PATCH 31/68] fix: add ~ to regex Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index f51d83e596..7b6088ae47 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -250,7 +250,7 @@ class SDJSONWebToken(Regexp): "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" ) - PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*(~Wy[a-zA-Z0-9]+)*$" + PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*(~Wy[~a-zA-Z0-9]+)*$" def __init__(self): """Initialize the instance.""" From 28fcd42e1bbd4a459595fa5bc4449c22404dac8d Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 25 Aug 2023 14:51:10 -0600 Subject: [PATCH 32/68] feat: invert sd list validation and examples Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 12 ++++++------ aries_cloudagent/wallet/routes.py | 15 ++++++++------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 7b6088ae47..b9fb578954 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -200,8 +200,8 @@ def __init__(self): ) -class SDList(Regexp): - """Validate SD List""" +class NonSDList(Regexp): + """Validate NonSD List""" EXAMPLE = [ "name", @@ -215,8 +215,8 @@ def __init__(self): """Initialize the instance.""" super().__init__( - SDList.PATTERN, - error="Value {input} is not a valid SDList", + NonSDList.PATTERN, + error="Value {input} is not a valid NonSDList", ) @@ -842,8 +842,8 @@ def __init__( JWS_HEADER_KID_VALIDATE = JWSHeaderKid() JWS_HEADER_KID_EXAMPLE = JWSHeaderKid.EXAMPLE -SD_LIST_VALIDATE = SDList() -SD_LIST_EXAMPLE = SDList().EXAMPLE +NON_SD_LIST_VALIDATE = NonSDList() +NON_SD_LIST_EXAMPLE = NonSDList().EXAMPLE JWT_VALIDATE = JSONWebToken() JWT_EXAMPLE = JSONWebToken.EXAMPLE diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index a8d1b81b6c..3ae9b16e5c 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -37,8 +37,8 @@ JWT_VALIDATE, SD_JWT_EXAMPLE, SD_JWT_VALIDATE, - SD_LIST_EXAMPLE, - SD_LIST_VALIDATE, + NON_SD_LIST_EXAMPLE, + NON_SD_LIST_VALIDATE, IndyDID, StrOrDictField, Uri, @@ -180,11 +180,11 @@ class JWSCreateSchema(OpenAPISchema): class SDJWSCreateSchema(JWSCreateSchema): """Request schema to create an sd-jws with a particular DID.""" - sd_list = fields.List( + non_sd_list = fields.List( fields.Str( required=False, - validate=SD_LIST_VALIDATE, - metadata={"example": SD_LIST_EXAMPLE}, + validate=NON_SD_LIST_VALIDATE, + metadata={"example": NON_SD_LIST_EXAMPLE}, ) ) @@ -999,6 +999,7 @@ async def wallet_sd_jwt_sign(request: web.BaseRequest): "did": "did:example:123", "verificationMethod": "did:example:123#keys-1" with did and verification being mutually exclusive. + "non_sd_list": [] """ context: AdminRequestContext = request["context"] body = await request.json() @@ -1006,11 +1007,11 @@ async def wallet_sd_jwt_sign(request: web.BaseRequest): verification_method = body.get("verificationMethod") headers = body.get("headers", {}) payload = body.get("payload", {}) - sd_list = body.get("sd_list", []) + non_sd_list = body.get("non_sd_list", []) try: sd_jws = await sd_jwt_sign( - context.profile, headers, payload, sd_list, did, verification_method + context.profile, headers, payload, non_sd_list, did, verification_method ) except ValueError as err: raise web.HTTPBadRequest(reason="Bad did or verification method") from err From f721cc35b9c00a95faa38b9f33d69216b6489bcd Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 25 Aug 2023 14:53:13 -0600 Subject: [PATCH 33/68] feat: method to create json paths from payload Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index b265df849d..8a37ab89c7 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -64,6 +64,36 @@ async def issue(self): self._create_combined() +def create_json_paths(it, current_path="", path_list=None): + """ + Create a json path for each element of the payload. + """ + if path_list is None: + path_list = [] + + if type(it) is dict: + for k, v in it.items(): + new_key = f"{current_path}.{k}" if current_path else k + path_list.append(new_key) + + if isinstance(v, dict): + create_json_paths(v, new_key, path_list) + elif isinstance(v, list): + for i, e in enumerate(v): + if isinstance(e, (dict, list)): + create_json_paths(e, f"{new_key}[{i}]", path_list) + else: + path_list.append(f"{new_key}[{i}]") + elif type(it) is list: + for i, e in enumerate(it): + if isinstance(e, (dict, list)): + create_json_paths(e, f"{current_path}[{i}]", path_list) + else: + path_list.append(f"{current_path}[{i}]") + + return path_list + + def sort_sd_list(sd_list): """ Sorts sd_list. From 514852009608efd9425457ee93b7f58574dacfbe Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 25 Aug 2023 14:55:55 -0600 Subject: [PATCH 34/68] feat: method to handle list splices Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 8a37ab89c7..52b141eb1f 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -1,6 +1,7 @@ """Operations supporting SD-JWT creation and verification.""" import json +import re from typing import Any, List, Mapping, Optional from marshmallow import fields from jsonpath_ng.ext import parse @@ -106,6 +107,24 @@ def sort_sd_list(sd_list): return [sd[1] for sd in nested_claim_sort] +def separate_list_splices(non_sd_list): + """ + Separate list splices in the non_sd_list into individual indices. + + This is necessary in order to properly construct the inverse of + the claims which should not be selectively disclosable in the case + of list splices. + """ + for item in non_sd_list: + if ":" in item: + split = re.split(r"\[|\]|:", item) + for i in range(int(split[1]), int(split[2])): + non_sd_list.append(f"{split[0]}[{i}]") + non_sd_list.remove(item) + + return non_sd_list + + async def sd_jwt_sign( profile: Profile, headers: Mapping[str, Any], From aa28ba12190a1995e7f1b9bb7a6ca4e6aef6adbf Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 25 Aug 2023 14:58:48 -0600 Subject: [PATCH 35/68] feat: invert sd handling such that sd is default Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 52b141eb1f..8f6b48920b 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -125,24 +125,36 @@ def separate_list_splices(non_sd_list): return non_sd_list +def create_sd_list(payload, non_sd_list): + """ + Create a list of claims which will be selectively disclosable. + """ + flattened_payload = create_json_paths(payload) + separated_non_sd_list = separate_list_splices(non_sd_list) + sd_list = [ + claim for claim in flattened_payload if claim not in separated_non_sd_list + ] + return sort_sd_list(sd_list) + + async def sd_jwt_sign( profile: Profile, headers: Mapping[str, Any], payload: Mapping[str, Any], - sd_list: List, + non_sd_list: List, did: Optional[str] = None, verification_method: Optional[str] = None, ) -> str: """ Sign sd-jwt. - Use sd_list to wrap selectively disclosable claims with - SDObj within payload, create SDJWTIssuerACAPy object, and - call SDJWTIssuerACAPy.issue(). + Use non_sd_list and json paths for payload elements to create a list of + claims that can be selectively disclosable. Use this list to wrap + selectively disclosable claims with SDObj within payload, + create SDJWTIssuerACAPy object, and call SDJWTIssuerACAPy.issue(). """ - - sorted_sd_list = sort_sd_list(sd_list) - for sd in sorted_sd_list: + sd_list = create_sd_list(payload, non_sd_list) + for sd in sd_list: jsonpath_expression = parse(f"$.{sd}") matches = jsonpath_expression.find(payload) if len(matches) < 1: From f0d0644b5b6fcad64dca67d652ce5d75205eeec4 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 25 Aug 2023 14:59:24 -0600 Subject: [PATCH 36/68] feat: add list of claims which are always visible Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 8f6b48920b..e5fb80e037 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -15,6 +15,9 @@ from ..messaging.valid import StrOrDictField +CLAIMS_NEVER_SD = ["iss", "iat", "exp", "cnf"] + + class SDJWTError(BaseError): """SD-JWT Error.""" @@ -161,13 +164,14 @@ async def sd_jwt_sign( raise SDJWTError(f"Claim for {sd} not found in payload.") else: for match in matches: - if type(match.context.value) is list: - match.context.value.remove(match.value) - match.context.value.append(SDObj(match.value)) - else: - match.context.value[ - SDObj(str(match.path)) - ] = match.context.value.pop(str(match.path)) + if str(match.path) not in CLAIMS_NEVER_SD: + if type(match.context.value) is list: + match.context.value.remove(match.value) + match.context.value.append(SDObj(match.value)) + else: + match.context.value[ + SDObj(str(match.path)) + ] = match.context.value.pop(str(match.path)) sd_jwt_issuer = SDJWTIssuerACAPy( user_claims=payload, From eb645cefdcccc2561a76fb2cf9c3abd310a9110e Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 25 Aug 2023 15:02:50 -0600 Subject: [PATCH 37/68] test: adjust tests for sd inversion Signed-off-by: Char Howland --- aries_cloudagent/wallet/tests/test_sd_jwt.py | 94 ++++++++++---------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/aries_cloudagent/wallet/tests/test_sd_jwt.py b/aries_cloudagent/wallet/tests/test_sd_jwt.py index 81a7daa5b6..fa97d211a4 100644 --- a/aries_cloudagent/wallet/tests/test_sd_jwt.py +++ b/aries_cloudagent/wallet/tests/test_sd_jwt.py @@ -16,7 +16,10 @@ def create_address_payload(): "locality": "Anytown", "region": "Anystate", "country": "US", - } + }, + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, } @@ -49,25 +52,22 @@ async def test_sign_with_did_key_and_verify(self, profile, in_memory_wallet): "birthdate": "1940-01-01", "updated_at": 1570000000, "nationalities": ["US", "DE", "SA"], + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, } - sd_list = [ - "address", - "address.street_address", - "address.street_address.house_number", - "address.locality", - "address.region", - "address.country", + non_sd_list = [ "given_name", "family_name", - "email", - "phone_number", - "phone_number_verified", - "birthdate", - "updated_at", - "nationalities[1:3]", + "nationalities", ] signed = await sd_jwt_sign( - profile, self.headers, payload, sd_list, did_info.did, verification_method + profile, + self.headers, + payload, + non_sd_list, + did_info.did, + verification_method, ) assert signed @@ -79,12 +79,19 @@ async def test_flat_structure( ): did_info = await in_memory_wallet.create_local_did(KEY, ED25519, self.seed) verification_method = None - sd_list = ["address"] + non_sd_list = [ + "address.street_address", + "address.street_address.house_number", + "address.street_address.street", + "address.locality", + "address.region", + "address.country", + ] signed = await sd_jwt_sign( profile, self.headers, create_address_payload, - sd_list, + non_sd_list, did_info.did, verification_method, ) @@ -94,10 +101,14 @@ async def test_flat_structure( assert isinstance(verified, SDJWTVerifyResult) assert verified.valid assert verified.payload["_sd"] - assert len(verified.payload["_sd"]) >= len(sd_list) assert verified.payload["_sd_alg"] - for disclosure in verified.disclosures: - assert disclosure[1] in sd_list + assert verified.disclosures[0][1] == "address" + assert verified.disclosures[0][2] == { + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US", + } @pytest.mark.asyncio async def test_nested_structure( @@ -105,18 +116,13 @@ async def test_nested_structure( ): did_info = await in_memory_wallet.create_local_did(KEY, ED25519, self.seed) verification_method = None - sd_list = [ - "address.street_address", - "address.locality", - "address.region", - "address.country", - ] + non_sd_list = ["address"] signed = await sd_jwt_sign( profile, self.headers, create_address_payload, - sd_list, + non_sd_list, did_info.did, verification_method, ) @@ -125,10 +131,10 @@ async def test_nested_structure( verified = await sd_jwt_verify(profile, signed) assert isinstance(verified, SDJWTVerifyResult) assert verified.valid - assert len(verified.payload["address"]["_sd"]) >= len(sd_list) + assert len(verified.payload["address"]["_sd"]) >= 4 assert verified.payload["_sd_alg"] - for disclosure in verified.disclosures: - assert f"address.{disclosure[1]}" in sd_list + sd_claims = ["street_address", "region", "locality", "country"] + assert sorted(sd_claims) == sorted([claim[1] for claim in verified.disclosures]) @pytest.mark.asyncio async def test_recursive_nested_structure( @@ -136,19 +142,13 @@ async def test_recursive_nested_structure( ): did_info = await in_memory_wallet.create_local_did(KEY, ED25519, self.seed) verification_method = None - sd_list = [ - "address", - "address.street_address", - "address.locality", - "address.region", - "address.country", - ] + non_sd_list = [] signed = await sd_jwt_sign( profile, self.headers, create_address_payload, - sd_list, + non_sd_list, did_info.did, verification_method, ) @@ -160,22 +160,28 @@ async def test_recursive_nested_structure( assert "address" not in verified.payload assert verified.payload["_sd"] assert verified.payload["_sd_alg"] + sd_claims = ["street_address", "region", "locality", "country"] for disclosure in verified.disclosures: if disclosure[1] == "address": assert isinstance(disclosure[2], dict) - assert len(disclosure[2]["_sd"]) >= len(sd_list) - 1 + assert len(disclosure[2]["_sd"]) >= 4 else: - assert f"address.{disclosure[1]}" in sd_list + assert disclosure[1] in sd_claims @pytest.mark.asyncio async def test_list_splice(self, profile, in_memory_wallet): did_info = await in_memory_wallet.create_local_did(KEY, ED25519, self.seed) payload = {"nationalities": ["US", "DE", "SA"]} verification_method = None - sd_list = ["nationalities[1:3]"] + non_sd_list = ["nationalities", "nationalities[1:3]"] signed = await sd_jwt_sign( - profile, self.headers, payload, sd_list, did_info.did, verification_method + profile, + self.headers, + payload, + non_sd_list, + did_info.did, + verification_method, ) assert signed @@ -189,6 +195,4 @@ async def test_list_splice(self, profile, in_memory_wallet): else: assert nationality in payload["nationalities"] assert verified.payload["_sd_alg"] - spliced = [element.value for element in payload["nationalities"][1:3]] - for disclosure in verified.disclosures: - assert disclosure[1] in spliced + assert verified.disclosures[0][1] == "US" From 677651bb2a7febe4d0d1a2421d21d2a822c990bb Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 25 Aug 2023 15:07:51 -0600 Subject: [PATCH 38/68] feat: default to empty list for non sd list Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index e5fb80e037..29092a2bf5 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -144,7 +144,7 @@ async def sd_jwt_sign( profile: Profile, headers: Mapping[str, Any], payload: Mapping[str, Any], - non_sd_list: List, + non_sd_list: List = [], did: Optional[str] = None, verification_method: Optional[str] = None, ) -> str: From aec0ac0912613fdc692ba27a80c2bec87ea1c4a9 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 25 Aug 2023 15:23:03 -0600 Subject: [PATCH 39/68] fix: flake8 Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 2 +- aries_cloudagent/wallet/jwt.py | 3 ++- aries_cloudagent/wallet/sd_jwt.py | 17 ++++++----------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index b9fb578954..27c32c6990 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -201,7 +201,7 @@ def __init__(self): class NonSDList(Regexp): - """Validate NonSD List""" + """Validate NonSD List.""" EXAMPLE = [ "name", diff --git a/aries_cloudagent/wallet/jwt.py b/aries_cloudagent/wallet/jwt.py index 34d3eb65d5..b6cf1a2bcc 100644 --- a/aries_cloudagent/wallet/jwt.py +++ b/aries_cloudagent/wallet/jwt.py @@ -105,6 +105,7 @@ def __init__( valid: bool, kid: str, ): + """Initialize a JWTVerifyResult instance.""" self.headers = headers self.payload = payload self.valid = valid @@ -112,7 +113,7 @@ def __init__( class JWTVerifyResultSchema(BaseModelSchema): - """JWTVerifyResult schema""" + """JWTVerifyResult schema.""" class Meta: """JWTVerifyResultSchema metadata.""" diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 29092a2bf5..0e8149a25a 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -69,9 +69,7 @@ async def issue(self): def create_json_paths(it, current_path="", path_list=None): - """ - Create a json path for each element of the payload. - """ + """Create a json path for each element of the payload.""" if path_list is None: path_list = [] @@ -129,9 +127,7 @@ def separate_list_splices(non_sd_list): def create_sd_list(payload, non_sd_list): - """ - Create a list of claims which will be selectively disclosable. - """ + """Create a list of claims which will be selectively disclosable.""" flattened_payload = create_json_paths(payload) separated_non_sd_list = separate_list_splices(non_sd_list) sd_list = [ @@ -188,7 +184,7 @@ async def sd_jwt_sign( class SDJWTVerifyResult(JWTVerifyResult): - """Result from verifying SD-JWT""" + """Result from verifying SD-JWT.""" class Meta: """SDJWTVerifyResult metadata.""" @@ -203,6 +199,7 @@ def __init__( kid, disclosures, ): + """Initialize an SDJWTVerifyResult instance.""" super().__init__( headers, payload, @@ -213,7 +210,7 @@ def __init__( class SDJWTVerifyResultSchema(JWTVerifyResultSchema): - """SDJWTVerifyResult schema""" + """SDJWTVerifyResult schema.""" class Meta: """SDJWTVerifyResultSchema metadata.""" @@ -303,9 +300,7 @@ async def verify(self): async def sd_jwt_verify( profile: Profile, sd_jwt_presentation: str ) -> SDJWTVerifyResult: - """ - Verify sd-jwt using SDJWTVerifierACAPy.verify(). - """ + """Verify sd-jwt using SDJWTVerifierACAPy.verify().""" sd_jwt_verifier = SDJWTVerifierACAPy(profile, sd_jwt_presentation) verified = await sd_jwt_verifier.verify() return verified From 74bcbe06c77f9fd50548cdcb896c27effc1ad51a Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 28 Aug 2023 13:07:03 -0600 Subject: [PATCH 40/68] fix: use atomic group to fix exponential backtracking issue with regex Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 27c32c6990..7ddba18b6b 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -250,7 +250,7 @@ class SDJSONWebToken(Regexp): "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" ) - PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*(~Wy[~a-zA-Z0-9]+)*$" + PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*(?>~Wy[~a-zA-Z0-9]+)*$" def __init__(self): """Initialize the instance.""" From d9df449e75c4d761a05f61ee43630c51566c41d3 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 28 Aug 2023 16:54:26 -0600 Subject: [PATCH 41/68] feat: update sd-jwt repo Signed-off-by: Char Howland --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e12e200aa8..cc6b92f957 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,4 +30,4 @@ qrcode[pil]~=6.1 requests~=2.31.0 rlp==1.2.0 unflatten~=0.1 -git+https://github.com/danielfett/sd-jwt.git@main +git+https://github.com/openwallet-foundation-labs/sd-jwt-python.git@main \ No newline at end of file From 6ad057cb8ec08a81c9ad53293b2fe2410f5d3109 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 28 Aug 2023 16:58:04 -0600 Subject: [PATCH 42/68] fix: type hints Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 0e8149a25a..1c49f06198 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -51,7 +51,7 @@ def __init__( self._serialization_format = serialization_format self.ii_disclosures = [] - async def _create_signed_jws(self): + async def _create_signed_jws(self) -> str: self.serialized_sd_jwt = await jwt_sign( self.profile, self.headers, @@ -60,7 +60,7 @@ async def _create_signed_jws(self): self.verification_method, ) - async def issue(self): + async def issue(self) -> str: """Issue an sd-jwt.""" self._check_for_sd_claim(self._user_claims) self._assemble_sd_jwt_payload() @@ -68,7 +68,7 @@ async def issue(self): self._create_combined() -def create_json_paths(it, current_path="", path_list=None): +def create_json_paths(it, current_path="", path_list=None) -> List: """Create a json path for each element of the payload.""" if path_list is None: path_list = [] @@ -96,7 +96,7 @@ def create_json_paths(it, current_path="", path_list=None): return path_list -def sort_sd_list(sd_list): +def sort_sd_list(sd_list) -> List: """ Sorts sd_list. @@ -108,7 +108,7 @@ def sort_sd_list(sd_list): return [sd[1] for sd in nested_claim_sort] -def separate_list_splices(non_sd_list): +def separate_list_splices(non_sd_list) -> List: """ Separate list splices in the non_sd_list into individual indices. @@ -126,7 +126,7 @@ def separate_list_splices(non_sd_list): return non_sd_list -def create_sd_list(payload, non_sd_list): +def create_sd_list(payload, non_sd_list) -> List: """Create a list of claims which will be selectively disclosable.""" flattened_payload = create_json_paths(payload) separated_non_sd_list = separate_list_splices(non_sd_list) From 8c9323e12615527bdc0511b449c0781f4f3507b4 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 28 Aug 2023 16:58:40 -0600 Subject: [PATCH 43/68] fix: use isinstance Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 1c49f06198..56cb399927 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -73,7 +73,7 @@ def create_json_paths(it, current_path="", path_list=None) -> List: if path_list is None: path_list = [] - if type(it) is dict: + if isinstance(it, dict): for k, v in it.items(): new_key = f"{current_path}.{k}" if current_path else k path_list.append(new_key) @@ -86,7 +86,7 @@ def create_json_paths(it, current_path="", path_list=None) -> List: create_json_paths(e, f"{new_key}[{i}]", path_list) else: path_list.append(f"{new_key}[{i}]") - elif type(it) is list: + elif isinstance(it, list): for i, e in enumerate(it): if isinstance(e, (dict, list)): create_json_paths(e, f"{current_path}[{i}]", path_list) @@ -161,7 +161,7 @@ async def sd_jwt_sign( else: for match in matches: if str(match.path) not in CLAIMS_NEVER_SD: - if type(match.context.value) is list: + if isinstance(match.context.value, list): match.context.value.remove(match.value) match.context.value.append(SDObj(match.value)) else: From 04edc50fac916975d5fbb677a6e39d7df9364c5c Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 28 Aug 2023 16:59:42 -0600 Subject: [PATCH 44/68] refactor: return sd_jwt_issuance from .issue() directly Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 46 +++++-------------------------- 1 file changed, 7 insertions(+), 39 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 56cb399927..369eab57f6 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -1,6 +1,5 @@ """Operations supporting SD-JWT creation and verification.""" -import json import re from typing import Any, List, Mapping, Optional from marshmallow import fields @@ -66,6 +65,7 @@ async def issue(self) -> str: self._assemble_sd_jwt_payload() await self._create_signed_jws() self._create_combined() + return self.sd_jwt_issuance def create_json_paths(it, current_path="", path_list=None) -> List: @@ -169,7 +169,7 @@ async def sd_jwt_sign( SDObj(str(match.path)) ] = match.context.value.pop(str(match.path)) - sd_jwt_issuer = SDJWTIssuerACAPy( + return await SDJWTIssuerACAPy( user_claims=payload, issuer_key=None, holder_key=None, @@ -177,10 +177,7 @@ async def sd_jwt_sign( headers=headers, did=did, verification_method=verification_method, - ) - await sd_jwt_issuer.issue() - - return sd_jwt_issuer.sd_jwt_issuance + ).issue() class SDJWTVerifyResult(JWTVerifyResult): @@ -250,7 +247,7 @@ def __init__( async def _verify_sd_jwt(self) -> SDJWTVerifyResult: verified = await jwt_verify( self.profile, - self.serialized_sd_jwt, + self._unverified_input_sd_jwt, ) return SDJWTVerifyResult( headers=verified.headers, @@ -260,40 +257,11 @@ async def _verify_sd_jwt(self) -> SDJWTVerifyResult: disclosures=self._disclosures_list, ) - def _parse_sd_jwt(self, sd_jwt): - if self._serialization_format == "compact": - ( - self._unverified_input_sd_jwt, - *self._input_disclosures, - self._unverified_input_key_binding_jwt, - ) = self._split(sd_jwt) - else: - # if the SD-JWT is in JSON format, parse the json and extract the disclosures. - self._unverified_input_sd_jwt = sd_jwt - self._unverified_input_sd_jwt_parsed = json.loads(sd_jwt) - self._input_disclosures = self._unverified_input_sd_jwt_parsed[ - self.JWS_KEY_DISCLOSURES - ] - self._unverified_input_key_binding_jwt = ( - self._unverified_input_sd_jwt_parsed.get(self.JWS_KEY_KB_JWT, "") - ) - - return self._unverified_input_sd_jwt - - def _create_disclosures_list(self) -> List: - disclosures_list = [] - for disclosure in self._input_disclosures: - disclosures_list.append( - json.loads(self._base64url_decode(disclosure).decode("utf-8")) - ) - - return disclosures_list - - async def verify(self): + async def verify(self) -> SDJWTVerifyResult: """Verify an sd-jwt.""" - self.serialized_sd_jwt = self._parse_sd_jwt(self.sd_jwt_presentation) + self._parse_sd_jwt(self.sd_jwt_presentation) self._create_hash_mappings(self._input_disclosures) - self._disclosures_list = self._create_disclosures_list() + self._disclosures_list = list(self._hash_to_decoded_disclosure.values()) return await self._verify_sd_jwt() From 0d28173ce88461a67e7991f8a48e8bf6ee304c7c Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 28 Aug 2023 17:01:41 -0600 Subject: [PATCH 45/68] test: include subset of SD claims in presentation Signed-off-by: Char Howland --- aries_cloudagent/wallet/tests/test_sd_jwt.py | 36 +++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/aries_cloudagent/wallet/tests/test_sd_jwt.py b/aries_cloudagent/wallet/tests/test_sd_jwt.py index fa97d211a4..b1425eb306 100644 --- a/aries_cloudagent/wallet/tests/test_sd_jwt.py +++ b/aries_cloudagent/wallet/tests/test_sd_jwt.py @@ -1,3 +1,5 @@ +from base64 import urlsafe_b64decode +import json import pytest from ...wallet.did_method import KEY @@ -41,10 +43,7 @@ async def test_sign_with_did_key_and_verify(self, profile, in_memory_wallet): "phone_number": "+1-202-555-0101", "phone_number_verified": True, "address": { - "street_address": { - "house_number": "123", - "street": "Main St", - }, + "street_address": "123 Main St", "locality": "Anytown", "region": "Anystate", "country": "US", @@ -59,7 +58,7 @@ async def test_sign_with_did_key_and_verify(self, profile, in_memory_wallet): non_sd_list = [ "given_name", "family_name", - "nationalities", + "birthdate", ] signed = await sd_jwt_sign( profile, @@ -71,7 +70,30 @@ async def test_sign_with_did_key_and_verify(self, profile, in_memory_wallet): ) assert signed - assert await sd_jwt_verify(profile, signed) + # Separate the jwt from the disclosures + signed_sd_jwt = signed.split("~")[0] + + # Determine which selectively disclosable attributes to reveal + revealed = ["sub", "phone_number", "phone_number_verified"] + + for disclosure in signed.split("~")[1:-1]: + # Decode the disclosures + padded = f"{disclosure}{'=' * divmod(len(disclosure),4)[1]}" + decoded = json.loads(urlsafe_b64decode(padded).decode("utf-8")) + # Add the disclosures associated with the claims to be revealed + if decoded[1] in revealed: + signed_sd_jwt = signed_sd_jwt + "~" + disclosure + + verified = await sd_jwt_verify(profile, f"{signed_sd_jwt}~") + assert verified.valid + # Validate that the non-selectively disclosable claims are visible in the payload + assert verified.payload["given_name"] == payload["given_name"] + assert verified.payload["family_name"] == payload["family_name"] + assert verified.payload["birthdate"] == payload["birthdate"] + # Validate that the revealed claims are in the disclosures + assert sorted(revealed) == sorted( + [disclosure[1] for disclosure in verified.disclosures] + ) @pytest.mark.asyncio async def test_flat_structure( @@ -81,8 +103,6 @@ async def test_flat_structure( verification_method = None non_sd_list = [ "address.street_address", - "address.street_address.house_number", - "address.street_address.street", "address.locality", "address.region", "address.country", From 3a4ca88ade83112c4b356f83f157fff5f287755d Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 28 Aug 2023 17:16:45 -0600 Subject: [PATCH 46/68] fix: syntax ?> not supported in the python re module Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 7ddba18b6b..9b63103be1 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -250,7 +250,7 @@ class SDJSONWebToken(Regexp): "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" ) - PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*(?>~Wy[~a-zA-Z0-9]+)*$" + PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*(?:~Wy[~a-zA-Z0-9]+)*$" def __init__(self): """Initialize the instance.""" From 3fca3843c94ff51df125716e2f6e921fd4a221d3 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Tue, 29 Aug 2023 10:05:25 -0600 Subject: [PATCH 47/68] fix: remove Wy prefix from sd-jwt regex Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 9b63103be1..476e528bb6 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -250,7 +250,7 @@ class SDJSONWebToken(Regexp): "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" ) - PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*(?:~Wy[~a-zA-Z0-9]+)*$" + PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*~[a-zA-Z0-9~]*$" def __init__(self): """Initialize the instance.""" From 2553a6b2745c4926f1bf365e3aaa3384ee1d97a8 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Tue, 29 Aug 2023 12:35:09 -0600 Subject: [PATCH 48/68] fix: ensure values between ~ delimiters in sd-jwt regex Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 476e528bb6..6607e1e1a5 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -250,7 +250,7 @@ class SDJSONWebToken(Regexp): "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" ) - PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*~[a-zA-Z0-9~]*$" + PATTERN = r"^[-_a-zA-Z0-9]+\.[-_a-zA-Z0-9]+\.[-_a-zA-Z0-9]+(?:~[a-zA-Z0-9]+)*~?$" def __init__(self): """Initialize the instance.""" From 31b7dc2546ab6b0a1532a79556d0c65c4768ded9 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Tue, 29 Aug 2023 12:48:07 -0600 Subject: [PATCH 49/68] fix: include full character set for urlsafe base64 encoded data Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 6607e1e1a5..342d5fb62f 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -250,7 +250,9 @@ class SDJSONWebToken(Regexp): "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" ) - PATTERN = r"^[-_a-zA-Z0-9]+\.[-_a-zA-Z0-9]+\.[-_a-zA-Z0-9]+(?:~[a-zA-Z0-9]+)*~?$" + PATTERN = ( + r"^[-_a-zA-Z0-9]+\.[-_a-zA-Z0-9]+\.[-_a-zA-Z0-9]+(?:~[a-zA-Z0-9\+/=\._-]+)*~?$" + ) def __init__(self): """Initialize the instance.""" From 95be51c81723443b16e70e41041adb35bd72f457 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Tue, 29 Aug 2023 13:19:13 -0600 Subject: [PATCH 50/68] fix: include full urlsafe b64 encoding character set for jwts Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 342d5fb62f..3188af5418 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -228,7 +228,7 @@ class JSONWebToken(Regexp): "eyJhIjogIjAifQ." "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" ) - PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*$" + PATTERN = r"^[a-zA-Z0-9\+/=\._-]+\.[a-zA-Z0-9\+/=\._-]+\.[a-zA-Z0-9\+/=\._-]+$" def __init__(self): """Initialize the instance.""" @@ -250,9 +250,8 @@ class SDJSONWebToken(Regexp): "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" ) - PATTERN = ( - r"^[-_a-zA-Z0-9]+\.[-_a-zA-Z0-9]+\.[-_a-zA-Z0-9]+(?:~[a-zA-Z0-9\+/=\._-]+)*~?$" - ) + PATTERN = r"""^[a-zA-Z0-9\+/=\._-]+\.[a-zA-Z0-9\+/=\._-]+\.[a-zA-Z0-9\+/=\._-]+ + (?:~[a-zA-Z0-9\+/=\._-]+)*~?$""" def __init__(self): """Initialize the instance.""" From 2986a77fadfdc480d27546ede0f0c380fe0747ba Mon Sep 17 00:00:00 2001 From: Char Howland Date: Tue, 29 Aug 2023 14:18:47 -0600 Subject: [PATCH 51/68] fix: remove some special characters from jwt regex Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 3188af5418..3487732e37 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -228,7 +228,7 @@ class JSONWebToken(Regexp): "eyJhIjogIjAifQ." "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" ) - PATTERN = r"^[a-zA-Z0-9\+/=\._-]+\.[a-zA-Z0-9\+/=\._-]+\.[a-zA-Z0-9\+/=\._-]+$" + PATTERN = r"^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$" def __init__(self): """Initialize the instance.""" @@ -250,8 +250,7 @@ class SDJSONWebToken(Regexp): "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" ) - PATTERN = r"""^[a-zA-Z0-9\+/=\._-]+\.[a-zA-Z0-9\+/=\._-]+\.[a-zA-Z0-9\+/=\._-]+ - (?:~[a-zA-Z0-9\+/=\._-]+)*~?$""" + PATTERN = r"^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+(?:~[a-zA-Z0-9_-]+)*~?$" def __init__(self): """Initialize the instance.""" From 889da11509a40229c4aaa2f03ebd411e3f63b3eb Mon Sep 17 00:00:00 2001 From: Char Howland Date: Tue, 29 Aug 2023 16:56:05 -0600 Subject: [PATCH 52/68] fix: jwt regex fix Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 3487732e37..ab1010f3b6 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -228,7 +228,7 @@ class JSONWebToken(Regexp): "eyJhIjogIjAifQ." "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" ) - PATTERN = r"^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$" + PATTERN = r"^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$" def __init__(self): """Initialize the instance.""" @@ -250,7 +250,7 @@ class SDJSONWebToken(Regexp): "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" ) - PATTERN = r"^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+(?:~[a-zA-Z0-9_-]+)*~?$" + PATTERN = r"^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+(?:~[a-zA-Z0-9_-]+)*~?$" def __init__(self): """Initialize the instance.""" From c7214c2c5efa32b6debd24e0136d7d447b16b015 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 7 Sep 2023 12:14:31 -0600 Subject: [PATCH 53/68] fix: prevent overwriting "typ" key in jwt_sign headers Signed-off-by: Char Howland --- aries_cloudagent/wallet/jwt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aries_cloudagent/wallet/jwt.py b/aries_cloudagent/wallet/jwt.py index b6cf1a2bcc..3137ec7bfa 100644 --- a/aries_cloudagent/wallet/jwt.py +++ b/aries_cloudagent/wallet/jwt.py @@ -69,10 +69,11 @@ async def jwt_sign( if not did: raise ValueError("DID URL must be absolute") + if not headers.get("typ", None): + headers["typ"] = "JWT" headers = { **headers, "alg": "EdDSA", - "typ": "JWT", "kid": verification_method, } encoded_headers = dict_to_b64(headers) From c84208f321895ac959e2d4fd3cdfed4216802ca6 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 7 Sep 2023 12:15:40 -0600 Subject: [PATCH 54/68] fix: prevent adding always visible claims to sd_jwt Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 38 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 369eab57f6..a1b9c11061 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -75,17 +75,18 @@ def create_json_paths(it, current_path="", path_list=None) -> List: if isinstance(it, dict): for k, v in it.items(): - new_key = f"{current_path}.{k}" if current_path else k - path_list.append(new_key) - - if isinstance(v, dict): - create_json_paths(v, new_key, path_list) - elif isinstance(v, list): - for i, e in enumerate(v): - if isinstance(e, (dict, list)): - create_json_paths(e, f"{new_key}[{i}]", path_list) - else: - path_list.append(f"{new_key}[{i}]") + if not k.startswith(tuple(CLAIMS_NEVER_SD)): + new_key = f"{current_path}.{k}" if current_path else k + path_list.append(new_key) + + if isinstance(v, dict): + create_json_paths(v, new_key, path_list) + elif isinstance(v, list): + for i, e in enumerate(v): + if isinstance(e, (dict, list)): + create_json_paths(e, f"{new_key}[{i}]", path_list) + else: + path_list.append(f"{new_key}[{i}]") elif isinstance(it, list): for i, e in enumerate(it): if isinstance(e, (dict, list)): @@ -160,14 +161,13 @@ async def sd_jwt_sign( raise SDJWTError(f"Claim for {sd} not found in payload.") else: for match in matches: - if str(match.path) not in CLAIMS_NEVER_SD: - if isinstance(match.context.value, list): - match.context.value.remove(match.value) - match.context.value.append(SDObj(match.value)) - else: - match.context.value[ - SDObj(str(match.path)) - ] = match.context.value.pop(str(match.path)) + if isinstance(match.context.value, list): + match.context.value.remove(match.value) + match.context.value.append(SDObj(match.value)) + else: + match.context.value[ + SDObj(str(match.path)) + ] = match.context.value.pop(str(match.path)) return await SDJWTIssuerACAPy( user_claims=payload, From 74cec2e0f523f11970d59b8c48d2480f23423273 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 7 Sep 2023 12:17:11 -0600 Subject: [PATCH 55/68] feat: add expected_nonce, expected_aud as arguments Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index a1b9c11061..1415204e8b 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -1,7 +1,7 @@ """Operations supporting SD-JWT creation and verification.""" import re -from typing import Any, List, Mapping, Optional +from typing import Any, List, Mapping, Optional, Union from marshmallow import fields from jsonpath_ng.ext import parse from sd_jwt.common import SDObj @@ -237,12 +237,16 @@ def __init__( self, profile: Profile, sd_jwt_presentation: str, + expected_aud: Union[str, None] = None, + expected_nonce: Union[str, None] = None, serialization_format: str = "compact", ): """Initialize an SDJWTVerifierACAPy instance.""" self.profile = profile self.sd_jwt_presentation = sd_jwt_presentation self._serialization_format = serialization_format + self.expected_aud = expected_aud + self.expected_nonce = expected_nonce async def _verify_sd_jwt(self) -> SDJWTVerifyResult: verified = await jwt_verify( @@ -266,9 +270,13 @@ async def verify(self) -> SDJWTVerifyResult: async def sd_jwt_verify( - profile: Profile, sd_jwt_presentation: str + profile: Profile, + sd_jwt_presentation: str, + expected_aud: str = None, + expected_nonce: str = None, ) -> SDJWTVerifyResult: """Verify sd-jwt using SDJWTVerifierACAPy.verify().""" - sd_jwt_verifier = SDJWTVerifierACAPy(profile, sd_jwt_presentation) - verified = await sd_jwt_verifier.verify() - return verified + sd_jwt_verifier = SDJWTVerifierACAPy( + profile, sd_jwt_presentation, expected_aud, expected_nonce + ) + return await sd_jwt_verifier.verify() From 47aabcae7e5311bc8d18886695fa5717b19b90f0 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 7 Sep 2023 12:19:06 -0600 Subject: [PATCH 56/68] feat: redefine _verify_key_binding_jwt Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 43 ++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 1415204e8b..817055e24f 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -266,7 +266,48 @@ async def verify(self) -> SDJWTVerifyResult: self._parse_sd_jwt(self.sd_jwt_presentation) self._create_hash_mappings(self._input_disclosures) self._disclosures_list = list(self._hash_to_decoded_disclosure.values()) - return await self._verify_sd_jwt() + + self.verified_sd_jwt = await self._verify_sd_jwt() + + if self.expected_aud or self.expected_nonce: + if not (self.expected_aud and self.expected_nonce): + raise ValueError( + "Either both expected_aud and expected_nonce must be provided " + "or both must be None" + ) + await self._verify_key_binding_jwt( + self.expected_aud, + self.expected_nonce, + ) + return self.verified_sd_jwt + + async def _verify_key_binding_jwt( + self, + expected_aud: Union[str, None] = None, + expected_nonce: Union[str, None] = None, + ): + verified_kb_jwt = await jwt_verify( + self.profile, self._unverified_input_key_binding_jwt + ) + self._holder_public_key_payload = self.verified_sd_jwt.payload.get("cnf", None) + + if not self._holder_public_key_payload: + raise ValueError("No holder public key in SD-JWT") + + holder_public_key_payload_jwk = self._holder_public_key_payload.get("jwk", None) + if not holder_public_key_payload_jwk: + raise ValueError( + "The holder_public_key_payload is malformed. " + "It doesn't contain the claim jwk: " + f"{self._holder_public_key_payload}" + ) + + if verified_kb_jwt.headers["typ"] != self.KB_JWT_TYP_HEADER: + raise ValueError("Invalid header typ") + if verified_kb_jwt.payload["aud"] != expected_aud: + raise ValueError("Invalid audience") + if verified_kb_jwt.payload["nonce"] != expected_nonce: + raise ValueError("Invalid nonce") async def sd_jwt_verify( From a7993fcee8a19803659ec6c396157307e005c6c8 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 7 Sep 2023 12:21:32 -0600 Subject: [PATCH 57/68] test: key binding implementation Signed-off-by: Char Howland --- aries_cloudagent/wallet/tests/test_sd_jwt.py | 223 +++++++++++++++++++ 1 file changed, 223 insertions(+) diff --git a/aries_cloudagent/wallet/tests/test_sd_jwt.py b/aries_cloudagent/wallet/tests/test_sd_jwt.py index b1425eb306..8e0cf10137 100644 --- a/aries_cloudagent/wallet/tests/test_sd_jwt.py +++ b/aries_cloudagent/wallet/tests/test_sd_jwt.py @@ -2,8 +2,10 @@ import json import pytest + from ...wallet.did_method import KEY from ...wallet.key_type import ED25519 +from ...wallet.jwt import jwt_sign from ..sd_jwt import SDJWTVerifyResult, sd_jwt_sign, sd_jwt_verify @@ -94,6 +96,9 @@ async def test_sign_with_did_key_and_verify(self, profile, in_memory_wallet): assert sorted(revealed) == sorted( [disclosure[1] for disclosure in verified.disclosures] ) + assert verified.payload["iss"] == payload["iss"] + assert verified.payload["iat"] == payload["iat"] + assert verified.payload["exp"] == payload["exp"] @pytest.mark.asyncio async def test_flat_structure( @@ -216,3 +221,221 @@ async def test_list_splice(self, profile, in_memory_wallet): assert nationality in payload["nationalities"] assert verified.payload["_sd_alg"] assert verified.disclosures[0][1] == "US" + + @pytest.mark.asyncio + async def test_sd_jwt_key_binding(self, profile, in_memory_wallet): + did_info = await in_memory_wallet.create_local_did(KEY, ED25519, self.seed) + verification_method = None + + payload = { + "given_name": "John", + "family_name": "Doe", + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ", + } + }, + } + signed = await sd_jwt_sign( + profile, + self.headers, + payload, + did=did_info.did, + verification_method=verification_method, + ) + assert signed + + # Key binding + headers_kb = {"alg": "ES256", "typ": "kb+jwt"} + payload_kb = { + "nonce": "1234567890", + "aud": "https://example.com/verifier", + "iat": 1688160483, + } + signed_kb = await jwt_sign( + profile, + headers_kb, + payload_kb, + did_info.did, + verification_method, + ) + assert signed_kb + + assert await sd_jwt_verify( + profile, + f"{signed}{signed_kb}", + expected_aud=payload_kb["aud"], + expected_nonce=payload_kb["nonce"], + ) + + test_input = [ + ( + "Either both expected_aud and expected_nonce must be provided or both must be None", + { + "given_name": "John", + "family_name": "Doe", + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ", + } + }, + }, + {"alg": "ES256", "typ": "kb+jwt"}, + "https://example.com/verifier", + None, + ), + ( + "No holder public key in SD-JWT", + { + "given_name": "John", + "family_name": "Doe", + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + }, + {"alg": "ES256", "typ": "kb+jwt"}, + "https://example.com/verifier", + "1234567890", + ), + ( + "The holder_public_key_payload is malformed. It doesn't contain the claim jwk: ", + { + "given_name": "John", + "family_name": "Doe", + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "cnf": {"y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ"}, + }, + {"alg": "ES256", "typ": "kb+jwt"}, + "https://example.com/verifier", + "1234567890", + ), + ( + "Invalid header typ", + { + "given_name": "John", + "family_name": "Doe", + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ", + } + }, + }, + {"alg": "ES256", "typ": "JWT"}, + "https://example.com/verifier", + "1234567890", + ), + ( + "Invalid audience", + { + "given_name": "John", + "family_name": "Doe", + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ", + } + }, + }, + {"alg": "ES256", "typ": "kb+jwt"}, + "invalid_aud", + "1234567890", + ), + ( + "Invalid nonce", + { + "given_name": "John", + "family_name": "Doe", + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ", + } + }, + }, + {"alg": "ES256", "typ": "kb+jwt"}, + "https://example.com/verifier", + "invalid_nonce", + ), + ] + + @pytest.mark.parametrize( + "error, payload, headers_kb, expected_aud, expected_nonce", test_input + ) + @pytest.mark.asyncio + async def test_sd_jwt_key_binding_errors( + self, + payload, + error, + expected_nonce, + headers_kb, + expected_aud, + profile, + in_memory_wallet, + ): + did_info = await in_memory_wallet.create_local_did(KEY, ED25519, self.seed) + verification_method = None + + signed = await sd_jwt_sign( + profile, + self.headers, + payload, + did=did_info.did, + verification_method=verification_method, + ) + assert signed + + # Key binding + payload_kb = { + "nonce": "1234567890", + "aud": "https://example.com/verifier", + "iat": 1688160483, + } + signed_kb = await jwt_sign( + profile, + headers_kb, + payload_kb, + did_info.did, + verification_method, + ) + assert signed_kb + + with pytest.raises( + ValueError, + match=error, + ): + await sd_jwt_verify( + profile, + f"{signed}{signed_kb}", + expected_aud=expected_aud, + expected_nonce=expected_nonce, + ) From 3141e459dee91a5f0c8a73816eaf69438d0bc1bf Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 7 Sep 2023 17:08:16 -0600 Subject: [PATCH 58/68] feat: add sd-jwt python library to pyproject.toml Signed-off-by: Char Howland --- poetry.lock | 305 +++++++++++++++++++++++++++++++++++++++---------- pyproject.toml | 1 + 2 files changed, 245 insertions(+), 61 deletions(-) diff --git a/poetry.lock b/poetry.lock index 03c7d1cf76..d57e8e0298 100644 --- a/poetry.lock +++ b/poetry.lock @@ -648,68 +648,113 @@ yaml = ["PyYAML"] [[package]] name = "coverage" -version = "7.3.0" +version = "7.3.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5"}, - {file = "coverage-7.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637"}, - {file = "coverage-7.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af"}, - {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1"}, - {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12"}, - {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689"}, - {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977"}, - {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51"}, - {file = "coverage-7.3.0-cp310-cp310-win32.whl", hash = "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527"}, - {file = "coverage-7.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1"}, - {file = "coverage-7.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f"}, - {file = "coverage-7.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d"}, - {file = "coverage-7.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd"}, - {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7"}, - {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a"}, - {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74"}, - {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214"}, - {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f"}, - {file = "coverage-7.3.0-cp311-cp311-win32.whl", hash = "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482"}, - {file = "coverage-7.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70"}, - {file = "coverage-7.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b"}, - {file = "coverage-7.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446"}, - {file = "coverage-7.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071"}, - {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe"}, - {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a"}, - {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873"}, - {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2"}, - {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b"}, - {file = "coverage-7.3.0-cp312-cp312-win32.whl", hash = "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321"}, - {file = "coverage-7.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479"}, - {file = "coverage-7.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1"}, - {file = "coverage-7.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd"}, - {file = "coverage-7.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e"}, - {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54"}, - {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254"}, - {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0"}, - {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84"}, - {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985"}, - {file = "coverage-7.3.0-cp38-cp38-win32.whl", hash = "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9"}, - {file = "coverage-7.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543"}, - {file = "coverage-7.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba"}, - {file = "coverage-7.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393"}, - {file = "coverage-7.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28"}, - {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95"}, - {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a"}, - {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34"}, - {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e"}, - {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54"}, - {file = "coverage-7.3.0-cp39-cp39-win32.whl", hash = "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3"}, - {file = "coverage-7.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e"}, - {file = "coverage-7.3.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0"}, - {file = "coverage-7.3.0.tar.gz", hash = "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865"}, + {file = "coverage-7.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3"}, + {file = "coverage-7.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276"}, + {file = "coverage-7.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e"}, + {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f"}, + {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392"}, + {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887"}, + {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d"}, + {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136"}, + {file = "coverage-7.3.1-cp310-cp310-win32.whl", hash = "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f"}, + {file = "coverage-7.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520"}, + {file = "coverage-7.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3"}, + {file = "coverage-7.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375"}, + {file = "coverage-7.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9"}, + {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593"}, + {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8"}, + {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0"}, + {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce"}, + {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3"}, + {file = "coverage-7.3.1-cp311-cp311-win32.whl", hash = "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a"}, + {file = "coverage-7.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c"}, + {file = "coverage-7.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc"}, + {file = "coverage-7.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832"}, + {file = "coverage-7.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969"}, + {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26"}, + {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204"}, + {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037"}, + {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760"}, + {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f"}, + {file = "coverage-7.3.1-cp312-cp312-win32.whl", hash = "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a"}, + {file = "coverage-7.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92"}, + {file = "coverage-7.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f"}, + {file = "coverage-7.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981"}, + {file = "coverage-7.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465"}, + {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344"}, + {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7"}, + {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40"}, + {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086"}, + {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff"}, + {file = "coverage-7.3.1-cp38-cp38-win32.whl", hash = "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3"}, + {file = "coverage-7.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e"}, + {file = "coverage-7.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1"}, + {file = "coverage-7.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6"}, + {file = "coverage-7.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4"}, + {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745"}, + {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7"}, + {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0"}, + {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0"}, + {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8"}, + {file = "coverage-7.3.1-cp39-cp39-win32.whl", hash = "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140"}, + {file = "coverage-7.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981"}, + {file = "coverage-7.3.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194"}, + {file = "coverage-7.3.1.tar.gz", hash = "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952"}, ] [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "41.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, + {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, + {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, + {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "cytoolz" version = "0.12.2" @@ -840,6 +885,23 @@ files = [ {file = "deepmerge-0.3.0.tar.gz", hash = "sha256:f6fd7f1293c535fb599e197e750dbe8674503c5d2a89759b3c72a3c46746d4fd"}, ] +[[package]] +name = "deprecated" +version = "1.2.14" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, + {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] + [[package]] name = "distlib" version = "0.3.7" @@ -1219,6 +1281,20 @@ decorator = "*" ply = "*" six = "*" +[[package]] +name = "jwcrypto" +version = "1.5.0" +description = "Implementation of JOSE Web standards" +optional = false +python-versions = ">= 3.6" +files = [ + {file = "jwcrypto-1.5.0.tar.gz", hash = "sha256:2c1dc51cf8e38ddf324795dfe9426dee9dd46caf47f535ccbc18781fba810b8d"}, +] + +[package.dependencies] +cryptography = ">=3.4" +deprecated = "*" + [[package]] name = "lxml" version = "4.9.3" @@ -2123,13 +2199,13 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] name = "pytest" -version = "7.4.1" +version = "7.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.1-py3-none-any.whl", hash = "sha256:460c9a59b14e27c602eb5ece2e47bec99dc5fc5f6513cf924a7d03a578991b1f"}, - {file = "pytest-7.4.1.tar.gz", hash = "sha256:2f2301e797521b23e4d2585a0a3d7b5e50fdddaaf7e7d6773ea26ddb17c213ab"}, + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [package.dependencies] @@ -2415,21 +2491,40 @@ files = [ {file = "ruff-0.0.285.tar.gz", hash = "sha256:45866048d1dcdcc80855998cb26c4b2b05881f9e043d2e3bfe1aa36d9a2e8f28"}, ] +[[package]] +name = "sd-jwt" +version = "0.9.1" +description = "The reference implementation of the IETF SD-JWT specification." +optional = false +python-versions = "^3.8" +files = [] +develop = false + +[package.dependencies] +jwcrypto = ">=1.3.1" +pyyaml = ">=5.4" + +[package.source] +type = "git" +url = "https://github.com/openwallet-foundation-labs/sd-jwt-python.git" +reference = "HEAD" +resolved_reference = "0d857bf9c4971d27f1e716814ba8b55d323caaaf" + [[package]] name = "setuptools" -version = "68.1.2" +version = "68.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.1.2-py3-none-any.whl", hash = "sha256:3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b"}, - {file = "setuptools-68.1.2.tar.gz", hash = "sha256:3d4dfa6d95f1b101d695a6160a7626e15583af71a5f52176efa5d39a054d475d"}, + {file = "setuptools-68.2.0-py3-none-any.whl", hash = "sha256:af3d5949030c3f493f550876b2fd1dd5ec66689c4ee5d5344f009746f71fd5a8"}, + {file = "setuptools-68.2.0.tar.gz", hash = "sha256:00478ca80aeebeecb2f288d3206b0de568df5cd2b8fada1209843cc9a8d88a48"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5,<=7.1.2)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "simplejson" @@ -2791,6 +2886,90 @@ frameworks = ["Django (>=1.11.16)", "Flask (>=0.12.2)", "aiohttp (>=3.0.0)", "bo lint = ["flake8 (==3.7.8)", "flake8-bugbear (==19.8.0)", "mypy (==0.730)", "pre-commit (>=1.17,<2.0)"] tests = ["Django (>=1.11.16)", "Flask (>=0.12.2)", "aiohttp (>=3.0.0)", "bottle (>=0.12.13)", "falcon (>=1.4.0,<2.0)", "mock", "pyramid (>=1.9.1)", "pytest", "pytest-aiohttp (>=0.3.0)", "tornado (>=4.5.2)", "webapp2 (>=3.0.0b1)", "webtest (==2.0.33)", "webtest-aiohttp (==2.0.0)"] +[[package]] +name = "wrapt" +version = "1.15.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, + {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, + {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, + {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, + {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, + {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, + {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, + {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, + {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, + {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, + {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, + {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, + {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, + {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, + {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, + {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, + {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, + {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, + {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +] + [[package]] name = "yarl" version = "1.9.2" @@ -2886,4 +3065,8 @@ indy = ["python3-indy"] [metadata] lock-version = "2.0" python-versions = "^3.9" +<<<<<<< HEAD content-hash = "3572d33f01ad13410e0ae03d925d0ab238b66e7c888ccfb29501f72f9bec66e7" +======= +content-hash = "cdc19dba91e323e22647314394ff3e805b87378762d9ac364cfd871d78604caf" +>>>>>>> feat: add sd-jwt python library to pyproject.toml diff --git a/pyproject.toml b/pyproject.toml index a634cc0ba7..ecf725499d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ unflatten="~0.1" asyncpg = ">=0.25.0,<0.26.0" web-py = ">=0.62,<1.0" pygments = ">=2.10,<3.0" +sd_jwt = {git = "https://github.com/openwallet-foundation-labs/sd-jwt-python.git"} # askar aries-askar= { version = "~0.2.5", optional = true } From 9acac83e0cc07d904a3cfea4f409a91c55457225 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 8 Sep 2023 09:09:30 -0600 Subject: [PATCH 59/68] chore: ruff Signed-off-by: Char Howland --- aries_cloudagent/wallet/routes.py | 6 ++---- aries_cloudagent/wallet/sd_jwt.py | 9 +++------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 3ae9b16e5c..22481b2d6a 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -990,8 +990,7 @@ async def wallet_jwt_sign(request: web.BaseRequest): @request_schema(SDJWSCreateSchema) @response_schema(WalletModuleResponseSchema(), description="") async def wallet_sd_jwt_sign(request: web.BaseRequest): - """ - Request handler for sd-jws creation using did. + """Request handler for sd-jws creation using did. Args: "headers": { ... }, @@ -1058,8 +1057,7 @@ async def wallet_jwt_verify(request: web.BaseRequest): @request_schema(SDJWSVerifySchema()) @response_schema(SDJWSVerifyResponseSchema(), 200, description="") async def wallet_sd_jwt_verify(request: web.BaseRequest): - """ - Request handler for sd-jws validation using did. + """Request handler for sd-jws validation using did. Args: "sd-jwt": { ... } diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 817055e24f..142ff2ca07 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -98,8 +98,7 @@ def create_json_paths(it, current_path="", path_list=None) -> List: def sort_sd_list(sd_list) -> List: - """ - Sorts sd_list. + """Sorts sd_list. Ensures that selectively disclosable claims deepest in the structure are handled first. @@ -110,8 +109,7 @@ def sort_sd_list(sd_list) -> List: def separate_list_splices(non_sd_list) -> List: - """ - Separate list splices in the non_sd_list into individual indices. + """Separate list splices in the non_sd_list into individual indices. This is necessary in order to properly construct the inverse of the claims which should not be selectively disclosable in the case @@ -145,8 +143,7 @@ async def sd_jwt_sign( did: Optional[str] = None, verification_method: Optional[str] = None, ) -> str: - """ - Sign sd-jwt. + """Sign sd-jwt. Use non_sd_list and json paths for payload elements to create a list of claims that can be selectively disclosable. Use this list to wrap From 1b8bc8b943df9f64f4133d1a0ac43d853b673d3c Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 8 Sep 2023 14:27:20 -0600 Subject: [PATCH 60/68] fix: move fixtures to conftest.py Signed-off-by: Char Howland --- aries_cloudagent/wallet/tests/conftest.py | 66 ++++++++++++++++++++ aries_cloudagent/wallet/tests/test_jwt.py | 65 +------------------ aries_cloudagent/wallet/tests/test_sd_jwt.py | 4 -- 3 files changed, 67 insertions(+), 68 deletions(-) create mode 100644 aries_cloudagent/wallet/tests/conftest.py diff --git a/aries_cloudagent/wallet/tests/conftest.py b/aries_cloudagent/wallet/tests/conftest.py new file mode 100644 index 0000000000..eed9e1aef7 --- /dev/null +++ b/aries_cloudagent/wallet/tests/conftest.py @@ -0,0 +1,66 @@ +import pytest +from aries_cloudagent.resolver.did_resolver import DIDResolver +from aries_cloudagent.resolver.tests.test_did_resolver import MockResolver +from aries_cloudagent.wallet.default_verification_key_strategy import ( + BaseVerificationKeyStrategy, + DefaultVerificationKeyStrategy, +) + +from ...core.in_memory.profile import InMemoryProfile +from ...wallet.did_method import DIDMethods +from ...wallet.in_memory import InMemoryWallet + + +@pytest.fixture() +async def profile(): + """In memory profile with injected dependencies.""" + + mock_sov = MockResolver( + ["key"], + resolved={ + "@context": "https://www.w3.org/ns/did/v1", + "id": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "verificationMethod": [ + { + "id": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "type": "Ed25519VerificationKey2018", + "controller": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "publicKeyBase58": "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", + } + ], + "authentication": [ + "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" + ], + "assertionMethod": [ + "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" + ], + "capabilityDelegation": [ + "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" + ], + "capabilityInvocation": [ + "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" + ], + "keyAgreement": [ + { + "id": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6LSbkodSr6SU2trs8VUgnrnWtSm7BAPG245ggrBmSrxbv1R", + "type": "X25519KeyAgreementKey2019", + "controller": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "publicKeyBase58": "5dTvYHaNaB7mk7iA9LqCJEHG2dGZQsvoi8WGzDRtYEf", + } + ], + }, + native=True, + ) + yield InMemoryProfile.test_profile( + bind={ + DIDMethods: DIDMethods(), + BaseVerificationKeyStrategy: DefaultVerificationKeyStrategy(), + DIDResolver: DIDResolver([mock_sov]), + } + ) + + +@pytest.fixture() +async def in_memory_wallet(profile): + """In memory wallet for testing.""" + yield InMemoryWallet(profile) diff --git a/aries_cloudagent/wallet/tests/test_jwt.py b/aries_cloudagent/wallet/tests/test_jwt.py index 289c65901f..b3a2b41f32 100644 --- a/aries_cloudagent/wallet/tests/test_jwt.py +++ b/aries_cloudagent/wallet/tests/test_jwt.py @@ -1,75 +1,12 @@ import pytest -from aries_cloudagent.resolver.did_resolver import DIDResolver -from aries_cloudagent.resolver.tests.test_did_resolver import MockResolver -from aries_cloudagent.wallet.default_verification_key_strategy import ( - BaseVerificationKeyStrategy, - DefaultVerificationKeyStrategy, -) from aries_cloudagent.wallet.key_type import ED25519 -from ...core.in_memory.profile import InMemoryProfile -from ...wallet.did_method import KEY, DIDMethods -from ...wallet.in_memory import InMemoryWallet +from ...wallet.did_method import KEY from ..jwt import jwt_sign, jwt_verify, resolve_public_key_by_kid_for_verify -@pytest.fixture() -async def profile(): - """In memory profile with injected dependencies.""" - - mock_sov = MockResolver( - ["key"], - resolved={ - "@context": "https://www.w3.org/ns/did/v1", - "id": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", - "verificationMethod": [ - { - "id": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", - "type": "Ed25519VerificationKey2018", - "controller": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", - "publicKeyBase58": "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", - } - ], - "authentication": [ - "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" - ], - "assertionMethod": [ - "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" - ], - "capabilityDelegation": [ - "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" - ], - "capabilityInvocation": [ - "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" - ], - "keyAgreement": [ - { - "id": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6LSbkodSr6SU2trs8VUgnrnWtSm7BAPG245ggrBmSrxbv1R", - "type": "X25519KeyAgreementKey2019", - "controller": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", - "publicKeyBase58": "5dTvYHaNaB7mk7iA9LqCJEHG2dGZQsvoi8WGzDRtYEf", - } - ], - }, - native=True, - ) - yield InMemoryProfile.test_profile( - bind={ - DIDMethods: DIDMethods(), - BaseVerificationKeyStrategy: DefaultVerificationKeyStrategy(), - DIDResolver: DIDResolver([mock_sov]), - } - ) - - -@pytest.fixture() -async def in_memory_wallet(profile): - """In memory wallet for testing.""" - yield InMemoryWallet(profile) - - class TestJWT: """Tests for JWT sign and verify using dids.""" diff --git a/aries_cloudagent/wallet/tests/test_sd_jwt.py b/aries_cloudagent/wallet/tests/test_sd_jwt.py index 8e0cf10137..9282c87b9c 100644 --- a/aries_cloudagent/wallet/tests/test_sd_jwt.py +++ b/aries_cloudagent/wallet/tests/test_sd_jwt.py @@ -2,15 +2,11 @@ import json import pytest - from ...wallet.did_method import KEY from ...wallet.key_type import ED25519 from ...wallet.jwt import jwt_sign - from ..sd_jwt import SDJWTVerifyResult, sd_jwt_sign, sd_jwt_verify -from .test_jwt import profile, in_memory_wallet - @pytest.fixture def create_address_payload(): From 11684e87153983f994a07f5f06fa25653f0b9bb0 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 8 Sep 2023 14:50:56 -0600 Subject: [PATCH 61/68] chore: poetry lock Signed-off-by: Char Howland --- poetry.lock | 184 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 155 insertions(+), 29 deletions(-) diff --git a/poetry.lock b/poetry.lock index d57e8e0298..f2cda674e2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry and should not be changed by hand. [[package]] name = "aiohttp" version = "3.8.5" description = "Async http client/server framework (asyncio)" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -112,6 +113,7 @@ speedups = ["Brotli", "aiodns", "cchardet"] name = "aiohttp-apispec" version = "2.2.3" description = "Build and document REST APIs with aiohttp and apispec" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -128,6 +130,7 @@ webargs = "<6.0" name = "aiohttp-cors" version = "0.7.0" description = "CORS support for aiohttp" +category = "main" optional = false python-versions = "*" files = [ @@ -142,6 +145,7 @@ aiohttp = ">=1.1" name = "aioredis" version = "2.0.1" description = "asyncio (PEP 3156) Redis support" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -160,6 +164,7 @@ hiredis = ["hiredis (>=1.0)"] name = "aiosignal" version = "1.3.1" description = "aiosignal: a list of registered asynchronous callbacks" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -174,6 +179,7 @@ frozenlist = ">=1.1.0" name = "alabaster" version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -185,6 +191,7 @@ files = [ name = "apispec" version = "3.3.2" description = "A pluggable API specification generator. Currently supports the OpenAPI Specification (f.k.a. the Swagger specification)." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -204,6 +211,7 @@ yaml = ["PyYAML (>=3.10)"] name = "aries-askar" version = "0.2.9" description = "" +category = "main" optional = true python-versions = ">=3.6.3" files = [ @@ -219,6 +227,7 @@ cached-property = ">=1.5,<2.0" name = "async-timeout" version = "4.0.3" description = "Timeout context manager for asyncio programs" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -230,6 +239,7 @@ files = [ name = "asyncpg" version = "0.25.0" description = "An asyncio PostgreSQL driver" +category = "main" optional = false python-versions = ">=3.6.0" files = [ @@ -270,6 +280,7 @@ test = ["flake8 (>=3.9.2,<3.10.0)", "pycodestyle (>=2.7.0,<2.8.0)", "uvloop (>=0 name = "asynctest" version = "0.13.0" description = "Enhance the standard unittest package with features for testing asyncio libraries" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -281,6 +292,7 @@ files = [ name = "attrs" version = "23.1.0" description = "Classes Without Boilerplate" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -299,6 +311,7 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte name = "babel" version = "2.12.1" description = "Internationalization utilities" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -310,6 +323,7 @@ files = [ name = "base58" version = "2.1.1" description = "Base58 and Base58Check implementation." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -324,6 +338,7 @@ tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", " name = "bases" version = "0.2.1" description = "Python library for general Base-N encodings." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -342,6 +357,7 @@ dev = ["base58", "mypy", "pylint", "pytest", "pytest-cov"] name = "black" version = "23.7.0" description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -388,6 +404,7 @@ uvloop = ["uvloop (>=0.15.2)"] name = "cached-property" version = "1.5.2" description = "A decorator for caching properties in classes." +category = "main" optional = true python-versions = "*" files = [ @@ -399,6 +416,7 @@ files = [ name = "cachetools" version = "5.3.1" description = "Extensible memoizing collections and decorators" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -410,6 +428,7 @@ files = [ name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -421,6 +440,7 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." +category = "main" optional = false python-versions = "*" files = [ @@ -497,6 +517,7 @@ pycparser = "*" name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -508,6 +529,7 @@ files = [ name = "charset-normalizer" version = "3.2.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -592,6 +614,7 @@ files = [ name = "cheroot" version = "10.0.0" description = "Highly-optimized, pure-python HTTP server" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -610,6 +633,7 @@ docs = ["furo", "jaraco.packaging (>=3.2)", "python-dateutil", "sphinx (>=1.8.2) name = "click" version = "8.1.7" description = "Composable command line interface toolkit" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -624,6 +648,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -635,6 +660,7 @@ files = [ name = "configargparse" version = "1.5.5" description = "A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -650,6 +676,7 @@ yaml = ["PyYAML"] name = "coverage" version = "7.3.1" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -714,6 +741,7 @@ toml = ["tomli"] name = "cryptography" version = "41.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -759,6 +787,7 @@ test-randomorder = ["pytest-randomly"] name = "cytoolz" version = "0.12.2" description = "Cython implementation of Toolz: High performance functional utilities" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -867,6 +896,7 @@ cython = ["cython"] name = "decorator" version = "5.1.1" description = "Decorators for Humans" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -878,6 +908,7 @@ files = [ name = "deepmerge" version = "0.3.0" description = "a toolset to deeply merge python dictionaries." +category = "main" optional = false python-versions = ">=3" files = [ @@ -889,6 +920,7 @@ files = [ name = "deprecated" version = "1.2.14" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -906,6 +938,7 @@ dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] name = "distlib" version = "0.3.7" description = "Distribution utilities" +category = "dev" optional = false python-versions = "*" files = [ @@ -915,19 +948,21 @@ files = [ [[package]] name = "docutils" -version = "0.20.1" +version = "0.18.1" description = "Docutils -- Python Documentation Utilities" +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ - {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, - {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, + {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, + {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, ] [[package]] name = "ecdsa" version = "0.16.1" description = "ECDSA cryptographic signature library (pure python)" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -946,6 +981,7 @@ gmpy2 = ["gmpy2"] name = "eth-hash" version = "0.3.3" description = "eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3" +category = "main" optional = false python-versions = ">=3.5, <4" files = [ @@ -965,6 +1001,7 @@ test = ["pytest (==5.4.1)", "pytest-xdist", "tox (==3.14.6)"] name = "eth-typing" version = "2.3.0" description = "eth-typing: Common type annotations for ethereum python packages" +category = "main" optional = false python-versions = ">=3.5, <4" files = [ @@ -982,6 +1019,7 @@ test = ["pytest (>=4.4,<4.5)", "pytest-xdist", "tox (>=2.9.1,<3)"] name = "eth-utils" version = "1.10.0" description = "eth-utils: Common utility functions for python code that interacts with Ethereum" +category = "main" optional = false python-versions = ">=3.5,!=3.5.2,<4" files = [ @@ -1005,6 +1043,7 @@ test = ["hypothesis (>=4.43.0,<5.0.0)", "pytest (==5.4.1)", "pytest-xdist", "tox name = "exceptiongroup" version = "1.1.3" description = "Backport of PEP 654 (exception groups)" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1019,6 +1058,7 @@ test = ["pytest (>=6)"] name = "filelock" version = "3.12.2" description = "A platform independent file lock." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1034,6 +1074,7 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p name = "frozendict" version = "2.3.8" description = "A simple immutable dictionary" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1080,6 +1121,7 @@ files = [ name = "frozenlist" version = "1.4.0" description = "A list-like structure which implements collections.abc.MutableSequence" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1150,6 +1192,7 @@ files = [ name = "identify" version = "2.5.27" description = "File identification library for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1164,6 +1207,7 @@ license = ["ukkonen"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1175,6 +1219,7 @@ files = [ name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1186,6 +1231,7 @@ files = [ name = "indy-credx" version = "1.0.0" description = "" +category = "main" optional = true python-versions = ">=3.6.3" files = [ @@ -1199,6 +1245,7 @@ files = [ name = "indy-vdr" version = "0.3.4" description = "" +category = "main" optional = true python-versions = ">=3.6.3" files = [ @@ -1211,6 +1258,7 @@ files = [ name = "inflection" version = "0.5.1" description = "A port of Ruby on Rails inflector to Python" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1222,6 +1270,7 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1233,6 +1282,7 @@ files = [ name = "jaraco-functools" version = "3.9.0" description = "Functools like those found in stdlib" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1252,6 +1302,7 @@ testing = ["jaraco.classes", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-c name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1269,6 +1320,7 @@ i18n = ["Babel (>=2.7)"] name = "jsonpath-ng" version = "1.5.2" description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming." +category = "main" optional = false python-versions = "*" files = [ @@ -1285,6 +1337,7 @@ six = "*" name = "jwcrypto" version = "1.5.0" description = "Implementation of JOSE Web standards" +category = "main" optional = false python-versions = ">= 3.6" files = [ @@ -1299,6 +1352,7 @@ deprecated = "*" name = "lxml" version = "4.9.3" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" files = [ @@ -1406,6 +1460,7 @@ source = ["Cython (>=0.29.35)"] name = "markdown" version = "3.1.1" description = "Python implementation of Markdown." +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ @@ -1423,6 +1478,7 @@ testing = ["coverage", "pyyaml"] name = "markupsafe" version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1501,6 +1557,7 @@ files = [ name = "marshmallow" version = "3.20.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1521,6 +1578,7 @@ tests = ["pytest", "pytz", "simplejson"] name = "mock" version = "4.0.3" description = "Rolling backport of unittest.mock for all Pythons" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1537,6 +1595,7 @@ test = ["pytest (<5.4)", "pytest-cov"] name = "more-itertools" version = "10.1.0" description = "More routines for operating on iterables, beyond itertools" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1548,6 +1607,7 @@ files = [ name = "msgpack" version = "1.0.5" description = "MessagePack serializer" +category = "main" optional = false python-versions = "*" files = [ @@ -1620,6 +1680,7 @@ files = [ name = "multidict" version = "6.0.4" description = "multidict implementation" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1703,6 +1764,7 @@ files = [ name = "multiformats" version = "0.2.1" description = "Python implementation of multiformats protocols." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1724,6 +1786,7 @@ full = ["blake3", "mmh3", "pycryptodomex", "pysha3", "pyskein"] name = "multiformats-config" version = "0.2.0.post4" description = "Pre-loading configuration module for the 'multiformats' package." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1741,6 +1804,7 @@ dev = ["mypy", "pylint", "pytest", "pytest-cov"] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1752,6 +1816,7 @@ files = [ name = "nest-asyncio" version = "1.5.7" description = "Patch asyncio to allow nested event loops" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1763,6 +1828,7 @@ files = [ name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" +category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -1777,6 +1843,7 @@ setuptools = "*" name = "packaging" version = "23.1" description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1788,6 +1855,7 @@ files = [ name = "pathspec" version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1799,6 +1867,7 @@ files = [ name = "peerdid" version = "0.5.2" description = "PeerDID for Python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1818,6 +1887,7 @@ tests = ["pytest (==6.2.5)", "pytest-xdist (==2.3.0)"] name = "pillow" version = "10.0.0" description = "Python Imaging Library (Fork)" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1887,6 +1957,7 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa name = "platformdirs" version = "3.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1902,6 +1973,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co name = "pluggy" version = "1.3.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1917,6 +1989,7 @@ testing = ["pytest", "pytest-benchmark"] name = "ply" version = "3.11" description = "Python Lex & Yacc" +category = "main" optional = false python-versions = "*" files = [ @@ -1928,6 +2001,7 @@ files = [ name = "portalocker" version = "2.7.0" description = "Wraps the portalocker recipe for easy usage" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1947,6 +2021,7 @@ tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "p name = "pre-commit" version = "3.3.3" description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1965,6 +2040,7 @@ virtualenv = ">=20.10.0" name = "prompt-toolkit" version = "2.0.10" description = "Library for building powerful interactive command lines in Python" +category = "main" optional = false python-versions = ">=2.6,<3.0.0 || >=3.3.0" files = [ @@ -1981,6 +2057,7 @@ wcwidth = "*" name = "ptvsd" version = "4.3.2" description = "Remote debugging server for Python support in Visual Studio and Visual Studio Code" +category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -2015,6 +2092,7 @@ files = [ name = "pycparser" version = "2.21" description = "C parser in Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2026,6 +2104,7 @@ files = [ name = "pydantic" version = "1.9.2" description = "Data validation and settings management using python type hints" +category = "main" optional = false python-versions = ">=3.6.1" files = [ @@ -2077,6 +2156,7 @@ email = ["email-validator (>=1.0.3)"] name = "pydevd" version = "1.5.1" description = "PyDev.Debugger (used in PyDev, PyCharm and VSCode Python)" +category = "dev" optional = false python-versions = "*" files = [ @@ -2097,6 +2177,7 @@ files = [ name = "pydevd-pycharm" version = "193.6015.41" description = "PyCharm Debugger (used in PyCharm and PyDev)" +category = "dev" optional = false python-versions = "*" files = [ @@ -2107,6 +2188,7 @@ files = [ name = "pydid" version = "0.3.8" description = "Python library for validating, constructing, and representing DIDs and DID Documents" +category = "main" optional = false python-versions = ">=3.6.9,<4.0.0" files = [ @@ -2123,6 +2205,7 @@ typing-extensions = ">=4.0.0,<4.1.0" name = "pygments" version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2137,6 +2220,7 @@ plugins = ["importlib-metadata"] name = "pyjwt" version = "2.8.0" description = "JSON Web Token implementation in Python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2154,6 +2238,7 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] name = "pyld" version = "2.0.3" description = "Python implementation of the JSON-LD API" +category = "main" optional = false python-versions = "*" files = [ @@ -2175,6 +2260,7 @@ requests = ["requests"] name = "pynacl" version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2201,6 +2287,7 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] name = "pytest" version = "7.4.2" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2223,6 +2310,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-asyncio" version = "0.14.0" description = "Pytest support for asyncio." +category = "dev" optional = false python-versions = ">= 3.5" files = [ @@ -2240,6 +2328,7 @@ testing = ["async-generator (>=1.3)", "coverage", "hypothesis (>=5.7.1)"] name = "pytest-cov" version = "2.10.1" description = "Pytest plugin for measuring coverage." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -2258,6 +2347,7 @@ testing = ["fields", "hunter", "process-tests (==2.0.2)", "pytest-xdist", "six", name = "pytest-ruff" version = "0.1.1" description = "pytest plugin to check ruff requirements." +category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -2272,6 +2362,7 @@ ruff = ">=0.0.242" name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -2286,6 +2377,7 @@ six = ">=1.5" name = "python-json-logger" version = "2.0.7" description = "A python library adding a json log formatter" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2297,6 +2389,7 @@ files = [ name = "python3-indy" version = "1.16.0.post286" description = "This is the official SDK for Hyperledger Indy (https://www.hyperledger.org/projects), which provides a distributed-ledger-based foundation for self-sovereign identity (https://sovrin.org). The major artifact of the SDK is a c-callable library." +category = "main" optional = true python-versions = "*" files = [ @@ -2313,6 +2406,7 @@ test = ["base58", "pytest (<3.7)", "pytest-asyncio (==0.10.0)"] name = "pytz" version = "2021.1" description = "World timezone definitions, modern and historical" +category = "main" optional = false python-versions = "*" files = [ @@ -2324,6 +2418,7 @@ files = [ name = "pywin32" version = "306" description = "Python for Window Extensions" +category = "main" optional = false python-versions = "*" files = [ @@ -2347,6 +2442,7 @@ files = [ name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2355,7 +2451,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -2363,15 +2458,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -2388,7 +2476,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -2396,7 +2483,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2406,6 +2492,7 @@ files = [ name = "qrcode" version = "6.1" description = "QR Code image generator" +category = "main" optional = false python-versions = "*" files = [ @@ -2428,6 +2515,7 @@ test = ["mock", "pytest", "pytest-cov"] name = "requests" version = "2.31.0" description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2449,6 +2537,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "rlp" version = "1.2.0" description = "A package for Recursive Length Prefix encoding and decoding" +category = "main" optional = false python-versions = "*" files = [ @@ -2469,6 +2558,7 @@ test = ["hypothesis (==3.56.5)", "pytest (==3.3.2)", "tox (>=2.9.1,<3)"] name = "ruff" version = "0.0.285" description = "An extremely fast Python linter, written in Rust." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2495,6 +2585,7 @@ files = [ name = "sd-jwt" version = "0.9.1" description = "The reference implementation of the IETF SD-JWT specification." +category = "main" optional = false python-versions = "^3.8" files = [] @@ -2514,6 +2605,7 @@ resolved_reference = "0d857bf9c4971d27f1e716814ba8b55d323caaaf" name = "setuptools" version = "68.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2530,6 +2622,7 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "simplejson" version = "3.19.1" description = "Simple, fast, extensible JSON encoder/decoder for Python" +category = "main" optional = false python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2624,6 +2717,7 @@ files = [ name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2635,6 +2729,7 @@ files = [ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" optional = false python-versions = "*" files = [ @@ -2646,6 +2741,7 @@ files = [ name = "sphinx" version = "1.8.4" description = "Python documentation generator" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2674,25 +2770,44 @@ websupport = ["sqlalchemy (>=0.9)", "whoosh (>=2.0)"] [[package]] name = "sphinx-rtd-theme" -version = "0.5.1" +version = "1.3.0" description = "Read the Docs theme for Sphinx" +category = "dev" optional = false -python-versions = "*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "sphinx_rtd_theme-0.5.1-py2.py3-none-any.whl", hash = "sha256:fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113"}, - {file = "sphinx_rtd_theme-0.5.1.tar.gz", hash = "sha256:eda689eda0c7301a80cf122dad28b1861e5605cbf455558f3775e1e8200e83a5"}, + {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, + {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, ] [package.dependencies] -sphinx = "*" +docutils = "<0.19" +sphinx = ">=1.6,<8" +sphinxcontrib-jquery = ">=4,<5" [package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] +dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +description = "Extension to include jQuery on newer Sphinx releases" +category = "dev" +optional = false +python-versions = ">=2.7" +files = [ + {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, + {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, +] + +[package.dependencies] +Sphinx = ">=1.8" [[package]] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -2708,6 +2823,7 @@ test = ["pytest"] name = "sphinxcontrib-websupport" version = "1.2.4" description = "Sphinx API for Web Apps" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -2726,6 +2842,7 @@ test = ["Sphinx", "pytest", "sqlalchemy", "whoosh"] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2737,6 +2854,7 @@ files = [ name = "toolz" version = "0.12.0" description = "List processing tools and functional utilities" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2748,6 +2866,7 @@ files = [ name = "typing-extensions" version = "4.0.1" description = "Backported and Experimental Type Hints for Python 3.6+" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2759,6 +2878,7 @@ files = [ name = "typing-validation" version = "1.0.0.post2" description = "A simple library for runtime type-checking." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2773,6 +2893,7 @@ dev = ["mypy", "pylint", "pytest", "pytest-cov", "rich"] name = "unflatten" version = "0.1.1" description = "Unflatten dict to dict with nested dict/arrays" +category = "main" optional = false python-versions = "*" files = [ @@ -2784,6 +2905,7 @@ files = [ name = "urllib3" version = "2.0.4" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2801,6 +2923,7 @@ zstd = ["zstandard (>=0.18.0)"] name = "ursa-bbs-signatures" version = "1.0.1" description = "" +category = "main" optional = true python-versions = ">=3.6.3" files = [ @@ -2813,6 +2936,7 @@ files = [ name = "varint" version = "1.0.2" description = "Simple python varint implementation" +category = "main" optional = false python-versions = "*" files = [ @@ -2821,13 +2945,14 @@ files = [ [[package]] name = "virtualenv" -version = "20.24.4" +version = "20.24.5" description = "Virtual Python Environment builder" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.4-py3-none-any.whl", hash = "sha256:29c70bb9b88510f6414ac3e55c8b413a1f96239b6b789ca123437d5e892190cb"}, - {file = "virtualenv-20.24.4.tar.gz", hash = "sha256:772b05bfda7ed3b8ecd16021ca9716273ad9f4467c801f27e83ac73430246dca"}, + {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, + {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, ] [package.dependencies] @@ -2843,6 +2968,7 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess name = "wcwidth" version = "0.2.6" description = "Measures the displayed width of unicode strings in a terminal" +category = "main" optional = false python-versions = "*" files = [ @@ -2854,6 +2980,7 @@ files = [ name = "web-py" version = "0.62" description = "web.py: makes web apps" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2867,6 +2994,7 @@ cheroot = "*" name = "webargs" version = "5.5.3" description = "Declarative parsing and validation of HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, webapp2, Falcon, and aiohttp." +category = "main" optional = false python-versions = "*" files = [ @@ -2890,6 +3018,7 @@ tests = ["Django (>=1.11.16)", "Flask (>=0.12.2)", "aiohttp (>=3.0.0)", "bottle name = "wrapt" version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -2974,6 +3103,7 @@ files = [ name = "yarl" version = "1.9.2" description = "Yet another URL library" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3065,8 +3195,4 @@ indy = ["python3-indy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -<<<<<<< HEAD -content-hash = "3572d33f01ad13410e0ae03d925d0ab238b66e7c888ccfb29501f72f9bec66e7" -======= -content-hash = "cdc19dba91e323e22647314394ff3e805b87378762d9ac364cfd871d78604caf" ->>>>>>> feat: add sd-jwt python library to pyproject.toml +content-hash = "72c46c5cf767dcba25d4bfc978b4d71d5f5aa1b1225813975964ae6925b84ec2" From 9d3f013b50cdae4067dd00d1fa3bf363789d630a Mon Sep 17 00:00:00 2001 From: Char Howland Date: Fri, 8 Sep 2023 15:38:30 -0600 Subject: [PATCH 62/68] chore: remove requirements.txt Signed-off-by: Char Howland --- requirements.txt | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index cc6b92f957..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,33 +0,0 @@ -aiohttp~=3.8.1 -aiohttp-apispec~=2.2.1 -aiohttp-cors~=0.7.0 -aioredis~=2.0.0 -apispec~=3.3.0 -async-timeout~=4.0.2 -base58~=2.1.0 -ConfigArgParse~=1.5.3 -deepmerge~=0.3.0 -ecdsa~=0.16.1 -jsonpath_ng==1.5.2 -Markdown~=3.1.1 -markupsafe==2.0.1 -marshmallow~=3.20.1 -msgpack~=1.0 -multiformats~=0.2.1 -nest_asyncio~=1.5.5 -packaging~=23.1 -portalocker~=2.7.0 -prompt_toolkit~=2.0.9 -pydid~=0.3.6 -pyjwt~=2.8.0 -pyld~=2.0.3 -pynacl~=1.5.0 -python-dateutil~=2.8.1 -python-json-logger~=2.0.7 -pytz~=2021.1 -pyyaml~=6.0.1 -qrcode[pil]~=6.1 -requests~=2.31.0 -rlp==1.2.0 -unflatten~=0.1 -git+https://github.com/openwallet-foundation-labs/sd-jwt-python.git@main \ No newline at end of file From 10eac810e263e9fe3c03aa2d9333d246658ed79d Mon Sep 17 00:00:00 2001 From: Char Howland Date: Wed, 13 Sep 2023 12:01:06 -0600 Subject: [PATCH 63/68] docs: sd jwt implementation Signed-off-by: Char Howland --- .../SelectiveDisclosureJWTs.md | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md diff --git a/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md b/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md new file mode 100644 index 0000000000..305da98f30 --- /dev/null +++ b/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md @@ -0,0 +1,89 @@ +# SD-JWT Implementation in ACA-Py + +This document describes the implementation of SD-JWTs in ACA-Py according to the [Selective Disclosure for JWTs (SD-JWT) Specification](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-05), which defines a mechanism for selective disclosure of individual elements of a JSON object used as the payload of a JSON Web Signature structure. + +This implementation adds an important privacy-preserving feature to JWTs, since the receiver of an unencrypted JWT can view all claims within. This feature allows the holder to present only a relevant subset of the claims for a given presentation. The issuer includes plaintext claims, called disclosures, outside of the JWT. Each disclosure corresponds to a hidden claim within the JWT. When a holder prepares a presentation, they include along with the JWT only the disclosures corresponding to the claims they wish to reveal. The verifier verifies that the disclosures in fact correspond to claim values within the issuer-signed JWT. The verifier cannot view the claim values not disclosed by the holder. + +In addition, this implementation includes an optional mechanism for key binding, which is the concept of binding an SD-JWT to a holder's public key and requiring that the holder prove possession of the corresponding private key when presenting the SD-JWT. + +## Issuer Instructions + +The issuer determines which claims in an SD-JWT can be selectively disclosable. In this implementation, all claims at all levels of the JSON structure are by default selectively disclosable. If the issuer wishes for certain claims to always be visible, they can indicate which claims should not be selectively disclosable, as described below. Essential verification data such as `iss`, `iat`, `exp`, and `cnf` are always visible. + +The issuer creates a list of JSON paths for the claims that will not be selectively disclosable. Here is an example payload: +``` +{ + "birthdate": "1940-01-01", + "address": { + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US", + }, + "nationalities": ["US", "DE", "SA"], +} + +``` + +| Attribute to access | JSON path | +|--------------|-----------| +| "birthdate" | "birthdate" | +| The country attribute within the address dictionary | "address.country" | +| The second item in the nationalities list | "nationalities[1] | +| All items in the nationalities list | "nationalities[0:2]" | + +The (specification)[https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-05#name-nested-data-in-sd-jwts] defines options for how the issuer can handle nested structures with respect to selective disclosability. As mentioned, all claims at all levels of the JSON structure are by default selectively disclosable. + +### [Option 1: Flat SD-JWT](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-05#section-5.7.1) +The issuer can decide to treat the `address` claim in the above example payload as a block that can either be disclosed completely or not at all. + +The issuer lists out all the claims inside "address" in the `non_sd_list`, but not `address` itself: +``` +non_sd_list = [ + "address.street_address", + "address.locality", + "address.region", + "address.country", +] +``` + +### [Option 2: Structured SD-JWT](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-05#section-5.7.2) +The issuer may instead decide to make the `address` claim contents selectively disclosable individually. + +The issuer lists only "address" in the `non_sd_list`. +``` +non_sd_list = ["address"] +``` + +### [Option 3: SD-JWT with Recursive Disclosures](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-05#section-5.7.3) +The issuer may also decide to make the `address` claim contents selectively disclosable recursively, i.e., the `address` claim is made selectively disclosable as well as its sub-claims. + +The issuer lists neither `address` nor the subclaims of `address` in the `non_sd_list`, leaving all with their default selective disclosability. If all claims can be selectively disclosable, the `non_sd_list` need not be defined explicitly. + + +## Walk-Through of SD-JWT Implementation + +### Signing SD-JWTs +THe `sd_jwt_sign` method: +- Creates the list of claims that are selectively disclosable + - Uses the `non_sd_list` compared against the list of JSON paths for all claims to create the list of JSON paths for selectively disclosable claims + - Separates list splices if necessary + - Sorts the `sd_list` so that the claims deepest in the structure are handled first + - Since we will wrap the selectively disclosable claim keys, the JSON paths for nested structures do not work properly when the claim key is wrapped in an object +- Uses the JSON paths in the `sd_list` to find each selectively disclosable claim and wrap it in the `SDObj` defined by the [sd-jwt Python library](https://github.com/openwallet-foundation-labs/sd-jwt-python) and removes/replaces the original entry + - For list items, the element itself is wrapped + - For other objects, the dictionary key is wrapped +- With this modified payload, the `SDJWTIssuerACAPy.issue()` method: + - Checks if there are selectively disclosable claims at any level in the payload + - Assembles the SD-JWT payload and creates the disclosures + - Calls `SDJWTIssuerACAPy._create_signed_jws()`, which is redefined in order to use the ACA-Py `jwt_sign` method and which creates the JWT + - Combines and returns the signed JWT with its disclosures and option key binding JWT, as indicated in the [specification](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-05#name-sd-jwt-structure) + + +### Verifying SD-JWTs +`sd_jwt_verify`: +- Parses the SD-JWT presentation into its component parts: JWT, disclosures, and optional key binding + - The JWT payload is parsed from its headers and signature +- Creates a list of plaintext disclosures +- Calls `SDJWTVerifierACAPy._verify_sd_jwt`, which is redefined in order to use the ACA-Py `jwt_verify` method, and which returns the verified JWT +- If key binding is used, the key binding JWT is verified and checked against the expected audience and nonce values From 3c749527335525aea78d2754261282d3fe061336 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Wed, 13 Sep 2023 12:20:37 -0600 Subject: [PATCH 64/68] docs: typo Signed-off-by: Char Howland --- docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md b/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md index 305da98f30..17fcde0340 100644 --- a/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md +++ b/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md @@ -64,7 +64,7 @@ The issuer lists neither `address` nor the subclaims of `address` in the `non_sd ## Walk-Through of SD-JWT Implementation ### Signing SD-JWTs -THe `sd_jwt_sign` method: +The `sd_jwt_sign` method: - Creates the list of claims that are selectively disclosable - Uses the `non_sd_list` compared against the list of JSON paths for all claims to create the list of JSON paths for selectively disclosable claims - Separates list splices if necessary @@ -81,7 +81,7 @@ THe `sd_jwt_sign` method: ### Verifying SD-JWTs -`sd_jwt_verify`: +The `sd_jwt_verify` method: - Parses the SD-JWT presentation into its component parts: JWT, disclosures, and optional key binding - The JWT payload is parsed from its headers and signature - Creates a list of plaintext disclosures From 522a6c1dc919dd5a18a7ec332100c36974490d63 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 14 Sep 2023 11:49:18 -0600 Subject: [PATCH 65/68] fix: sd-jwt regex to allow key binding JWT Signed-off-by: Char Howland --- aries_cloudagent/messaging/valid.py | 2 +- aries_cloudagent/wallet/routes.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index ab1010f3b6..52fb123f35 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -250,7 +250,7 @@ class SDJSONWebToken(Regexp): "~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0" "~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ" ) - PATTERN = r"^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+(?:~[a-zA-Z0-9_-]+)*~?$" + PATTERN = r"^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+(?:~[a-zA-Z0-9._-]+)*~?$" def __init__(self): """Initialize the instance.""" diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 22481b2d6a..e440ba9469 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -1052,7 +1052,9 @@ async def wallet_jwt_verify(request: web.BaseRequest): @docs( - tags=["wallet"], summary="Verify a EdDSA sd-jws using did keys with a given SD-JWS" + tags=["wallet"], + summary="Verify a EdDSA sd-jws using did keys with a given SD-JWS with " + "optional key binding", ) @request_schema(SDJWSVerifySchema()) @response_schema(SDJWSVerifyResponseSchema(), 200, description="") From 636ed4445a1924c7d54616b3e1afac152e6e3c0f Mon Sep 17 00:00:00 2001 From: Char Howland Date: Thu, 14 Sep 2023 11:50:33 -0600 Subject: [PATCH 66/68] docs: example inputs to admin api endpoints Signed-off-by: Char Howland --- .../SelectiveDisclosureJWTs.md | 109 +++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md b/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md index 17fcde0340..2472c709a3 100644 --- a/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md +++ b/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md @@ -64,7 +64,47 @@ The issuer lists neither `address` nor the subclaims of `address` in the `non_sd ## Walk-Through of SD-JWT Implementation ### Signing SD-JWTs -The `sd_jwt_sign` method: + +#### Example input to `/wallet/sd-jwt/sign` endpoint: + +``` +{ + "did": "WpVJtxKVwGQdRpQP8iwJZy", + "headers": {}, + "payload": { + "sub": "user_42", + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "phone_number_verified": true, + "address": { + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US" + }, + "birthdate": "1940-01-01", + "updated_at": 1570000000, + "nationalities": ["US", "DE", "SA"], + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000 + }, + "non_sd_list": [ + "given_name", + "family_name", + "nationalities" + ] +} + +``` +#### Output: +``` +"eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJFZERTQSIsICJraWQiOiAiZGlkOnNvdjpXcFZKdHhLVndHUWRScFFQOGl3Slp5I2tleS0xIn0.eyJfc2QiOiBbIkR0a21ha3NkZGtHRjFKeDBDY0kxdmxRTmZMcGFnQWZ1N3p4VnBGRWJXeXciLCAiSlJLb1E0QXVHaU1INWJIanNmNVV4YmJFeDh2YzFHcUtvX0l3TXE3Nl9xbyIsICJNTTh0TlVLNUstR1lWd0swX01kN0k4MzExTTgwVi13Z0hRYWZvRkoxS09JIiwgIlBaM1VDQmdadVRMMDJkV0pxSVY4elUtSWhnalJNX1NTS3dQdTk3MURmLTQiLCAiX294WGNuSW5Yai1SV3BMVHNISU5YaHFrRVAwODkwUFJjNDBISWE1NElJMCIsICJhdnRLVW5Sdnc1clV0TnZfUnAwUll1dUdkR0RzcnJPYWJfVjR1Y05RRWRvIiwgInByRXZJbzBseTVtNTVsRUpTQUdTVzMxWGdVTElOalo5ZkxiRG81U1pCX0UiXSwgImdpdmVuX25hbWUiOiAiSm9obiIsICJmYW1pbHlfbmFtZSI6ICJEb2UiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJPdU1wcEhpYzEySjYzWTBIY2Ffd1BVeDJCTGdUQVdZQjJpdXpMY3lvcU5JIn0sIHsiLi4uIjogIlIxczlaU3NYeVV0T2QyODdEYy1DTVYyMEdvREF3WUVHV3c4ZkVKd1BNMjAifSwgeyIuLi4iOiAid0lJbjdhQlNDVkFZcUF1Rks3Nmpra3FjVGFvb3YzcUhKbzU5WjdKWHpnUSJ9XSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNjgzMDAwMDAwLCAiZXhwIjogMTg4MzAwMDAwMCwgIl9zZF9hbGciOiAic2hhLTI1NiJ9.cIsuGTIPfpRs_Z49nZcn7L6NUgxQumMGQpu8K6rBtv-YRiFyySUgthQI8KZe1xKyn5Wc8zJnRcWbFki2Vzw6Cw~WyJmWURNM1FQcnZicnZ6YlN4elJsUHFnIiwgIlNBIl0~WyI0UGc2SmZ0UnRXdGFPcDNZX2tscmZRIiwgIkRFIl0~WyJBcDh1VHgxbVhlYUgxeTJRRlVjbWV3IiwgIlVTIl0~WyJ4dkRYMDBmalpmZXJpTmlQb2Q1MXFRIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ~WyJYOTlzM19MaXhCY29yX2hudFJFWmNnIiwgInN1YiIsICJ1c2VyXzQyIl0~WyIxODVTak1hM1k3QlFiWUpabVE3U0NRIiwgInBob25lX251bWJlcl92ZXJpZmllZCIsIHRydWVd~WyJRN1FGaUpvZkhLSWZGV0kxZ0Vaal93IiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJOeWtVcmJYN1BjVE1ubVRkUWVxZXl3IiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlemJwQ2lnVlhrY205RlluVjNQMGJ3IiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJvd3ROX3I5Z040MzZKVnJFRWhQU05BIiwgInN0cmVldF9hZGRyZXNzIiwgIjEyMyBNYWluIFN0Il0~WyJLQXktZ0VaWmRiUnNHV1dNVXg5amZnIiwgInJlZ2lvbiIsICJBbnlzdGF0ZSJd~WyJPNnl0anM2SU9HMHpDQktwa0tzU1pBIiwgImxvY2FsaXR5IiwgIkFueXRvd24iXQ~WyI0Nzg5aG5GSjhFNTRsLW91RjRaN1V3IiwgImNvdW50cnkiLCAiVVMiXQ~WyIyaDR3N0FuaDFOOC15ZlpGc2FGVHRBIiwgImFkZHJlc3MiLCB7Il9zZCI6IFsiTXhKRDV5Vm9QQzFIQnhPRmVRa21TQ1E0dVJrYmNrellza1Z5RzVwMXZ5SSIsICJVYkxmVWlpdDJTOFhlX2pYbS15RHBHZXN0ZDNZOGJZczVGaVJpbVBtMHdvIiwgImhsQzJEYVBwT2t0eHZyeUFlN3U2YnBuM09IZ193Qk5heExiS3lPRDVMdkEiLCAia2NkLVJNaC1PaGFZS1FPZ2JaajhmNUppOXNLb2hyYnlhYzNSdXRqcHNNYyJdfV0~" +``` + +The `sd_jwt_sign()` method: - Creates the list of claims that are selectively disclosable - Uses the `non_sd_list` compared against the list of JSON paths for all claims to create the list of JSON paths for selectively disclosable claims - Separates list splices if necessary @@ -80,8 +120,73 @@ The `sd_jwt_sign` method: - Combines and returns the signed JWT with its disclosures and option key binding JWT, as indicated in the [specification](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-05#name-sd-jwt-structure) + ### Verifying SD-JWTs -The `sd_jwt_verify` method: + +#### Example input to `/wallet/sd-jwt/verify` endpoint: + +Using the output from the `/wallet/sd-jwt/sign` example above, we have decided to only reveal two of the selectively disclosable claims (`user` and `updated_at`) and achieved this by only including the disclosures for those claims. We have also included a key binding JWT following the disclosures. +``` +{ + "sd_jwt": "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJFZERTQSIsICJraWQiOiAiZGlkOnNvdjpXcFZKdHhLVndHUWRScFFQOGl3Slp5I2tleS0xIn0.eyJfc2QiOiBbIkR0a21ha3NkZGtHRjFKeDBDY0kxdmxRTmZMcGFnQWZ1N3p4VnBGRWJXeXciLCAiSlJLb1E0QXVHaU1INWJIanNmNVV4YmJFeDh2YzFHcUtvX0l3TXE3Nl9xbyIsICJNTTh0TlVLNUstR1lWd0swX01kN0k4MzExTTgwVi13Z0hRYWZvRkoxS09JIiwgIlBaM1VDQmdadVRMMDJkV0pxSVY4elUtSWhnalJNX1NTS3dQdTk3MURmLTQiLCAiX294WGNuSW5Yai1SV3BMVHNISU5YaHFrRVAwODkwUFJjNDBISWE1NElJMCIsICJhdnRLVW5Sdnc1clV0TnZfUnAwUll1dUdkR0RzcnJPYWJfVjR1Y05RRWRvIiwgInByRXZJbzBseTVtNTVsRUpTQUdTVzMxWGdVTElOalo5ZkxiRG81U1pCX0UiXSwgImdpdmVuX25hbWUiOiAiSm9obiIsICJmYW1pbHlfbmFtZSI6ICJEb2UiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJPdU1wcEhpYzEySjYzWTBIY2Ffd1BVeDJCTGdUQVdZQjJpdXpMY3lvcU5JIn0sIHsiLi4uIjogIlIxczlaU3NYeVV0T2QyODdEYy1DTVYyMEdvREF3WUVHV3c4ZkVKd1BNMjAifSwgeyIuLi4iOiAid0lJbjdhQlNDVkFZcUF1Rks3Nmpra3FjVGFvb3YzcUhKbzU5WjdKWHpnUSJ9XSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNjgzMDAwMDAwLCAiZXhwIjogMTg4MzAwMDAwMCwgIl9zZF9hbGciOiAic2hhLTI1NiJ9.cIsuGTIPfpRs_Z49nZcn7L6NUgxQumMGQpu8K6rBtv-YRiFyySUgthQI8KZe1xKyn5Wc8zJnRcWbFki2Vzw6Cw~WyJ4dkRYMDBmalpmZXJpTmlQb2Q1MXFRIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ~WyJYOTlzM19MaXhCY29yX2hudFJFWmNnIiwgInN1YiIsICJ1c2VyXzQyIl0~eyJhbGciOiAiRWREU0EiLCAidHlwIjogImtiK2p3dCIsICJraWQiOiAiZGlkOnNvdjpXcFZKdHhLVndHUWRScFFQOGl3Slp5I2tleS0xIn0.eyJub25jZSI6ICIxMjM0NTY3ODkwIiwgImF1ZCI6ICJodHRwczovL2V4YW1wbGUuY29tL3ZlcmlmaWVyIiwgImlhdCI6IDE2ODgxNjA0ODN9.i55VeR7bNt7T8HWJcfj6jSLH3Q7vFk8N0t7Tb5FZHKmiHyLrg0IPAuK5uKr3_4SkjuGt1_iNl8Wr3atWBtXMDA" +} +``` + +#### Output: +Note that attributes in the `non_sd_list` (`given_name`, `family_name`, and `nationalities`), as well as essential verification data (`iss`, `iat`, `exp`) are visible directly within the payload. The disclosures include only the values for the `user` and `updated_at` claims, since those are the only selectively disclosable claims that the holder presented. The corresponding hashes for those disclosures appear in the `payload["_sd"]` list. +``` +{ + "headers": { + "typ": "JWT", + "alg": "EdDSA", + "kid": "did:sov:WpVJtxKVwGQdRpQP8iwJZy#key-1" + }, + "payload": { + "_sd": [ + "DtkmaksddkGF1Jx0CcI1vlQNfLpagAfu7zxVpFEbWyw", + "JRKoQ4AuGiMH5bHjsf5UxbbEx8vc1GqKo_IwMq76_qo", + "MM8tNUK5K-GYVwK0_Md7I8311M80V-wgHQafoFJ1KOI", + "PZ3UCBgZuTL02dWJqIV8zU-IhgjRM_SSKwPu971Df-4", + "_oxXcnInXj-RWpLTsHINXhqkEP0890PRc40HIa54II0", + "avtKUnRvw5rUtNv_Rp0RYuuGdGDsrrOab_V4ucNQEdo", + "prEvIo0ly5m55lEJSAGSW31XgULINjZ9fLbDo5SZB_E" + ], + "given_name": "John", + "family_name": "Doe", + "nationalities": [ + { + "...": "OuMppHic12J63Y0Hca_wPUx2BLgTAWYB2iuzLcyoqNI" + }, + { + "...": "R1s9ZSsXyUtOd287Dc-CMV20GoDAwYEGWw8fEJwPM20" + }, + { + "...": "wIIn7aBSCVAYqAuFK76jkkqcTaoov3qHJo59Z7JXzgQ" + } + ], + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "_sd_alg": "sha-256" + }, + "valid": true, + "kid": "did:sov:WpVJtxKVwGQdRpQP8iwJZy#key-1", + "disclosures": [ + [ + "xvDX00fjZferiNiPod51qQ", + "updated_at", + 1570000000 + ], + [ + "X99s3_LixBcor_hntREZcg", + "sub", + "user_42" + ] + ] +} +``` + +The `sd_jwt_verify()` method: - Parses the SD-JWT presentation into its component parts: JWT, disclosures, and optional key binding - The JWT payload is parsed from its headers and signature - Creates a list of plaintext disclosures From 5d0335fd6a28e7e0a041074d3d58abe845d93936 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 18 Sep 2023 21:52:28 -0600 Subject: [PATCH 67/68] chore: use alias for jsonpath parse Signed-off-by: Char Howland --- aries_cloudagent/wallet/sd_jwt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/wallet/sd_jwt.py b/aries_cloudagent/wallet/sd_jwt.py index 142ff2ca07..59f3291995 100644 --- a/aries_cloudagent/wallet/sd_jwt.py +++ b/aries_cloudagent/wallet/sd_jwt.py @@ -3,7 +3,7 @@ import re from typing import Any, List, Mapping, Optional, Union from marshmallow import fields -from jsonpath_ng.ext import parse +from jsonpath_ng.ext import parse as jsonpath_parse from sd_jwt.common import SDObj from sd_jwt.issuer import SDJWTIssuer from sd_jwt.verifier import SDJWTVerifier @@ -152,7 +152,7 @@ async def sd_jwt_sign( """ sd_list = create_sd_list(payload, non_sd_list) for sd in sd_list: - jsonpath_expression = parse(f"$.{sd}") + jsonpath_expression = jsonpath_parse(f"$.{sd}") matches = jsonpath_expression.find(payload) if len(matches) < 1: raise SDJWTError(f"Claim for {sd} not found in payload.") From 97f7d69bf37bb015b103cfb0f956958c7ad42074 Mon Sep 17 00:00:00 2001 From: Char Howland Date: Mon, 18 Sep 2023 22:13:47 -0600 Subject: [PATCH 68/68] docs: fix markdown link Signed-off-by: Char Howland --- docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md b/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md index 2472c709a3..ed21f04768 100644 --- a/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md +++ b/docs/GettingStartedAriesDev/SelectiveDisclosureJWTs.md @@ -32,7 +32,7 @@ The issuer creates a list of JSON paths for the claims that will not be selectiv | The second item in the nationalities list | "nationalities[1] | | All items in the nationalities list | "nationalities[0:2]" | -The (specification)[https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-05#name-nested-data-in-sd-jwts] defines options for how the issuer can handle nested structures with respect to selective disclosability. As mentioned, all claims at all levels of the JSON structure are by default selectively disclosable. +The [specification](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-05#name-nested-data-in-sd-jwts) defines options for how the issuer can handle nested structures with respect to selective disclosability. As mentioned, all claims at all levels of the JSON structure are by default selectively disclosable. ### [Option 1: Flat SD-JWT](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-05#section-5.7.1) The issuer can decide to treat the `address` claim in the above example payload as a block that can either be disclosed completely or not at all.