From 67374e69aedc0aa3d993687fc69568a127937738 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Mon, 18 Sep 2023 13:51:14 -0400 Subject: [PATCH 01/16] feat: add vc ldp manager This manager will be used to encapsulate the necessary operations to sign and verify credentials. Additional refactoring required to have it be reused from the json-ld routes and from ICv2. Additional implementation required to add verification. It may make sense to have it handle storing VCs as well. Signed-off-by: Daniel Bluhm --- .../purposes/authentication_proof_purpose.py | 8 +- .../vc/ld_proofs/purposes/proof_purpose.py | 8 +- .../suites/ed25519_signature_2018.py | 8 +- .../suites/ed25519_signature_2020.py | 8 +- .../suites/jws_linked_data_signature.py | 8 +- aries_cloudagent/vc/vc_ld/manager.py | 329 ++++++++++++++++++ aries_cloudagent/vc/vc_ld/models/options.py | 168 +++++++++ 7 files changed, 519 insertions(+), 18 deletions(-) create mode 100644 aries_cloudagent/vc/vc_ld/manager.py create mode 100644 aries_cloudagent/vc/vc_ld/models/options.py diff --git a/aries_cloudagent/vc/ld_proofs/purposes/authentication_proof_purpose.py b/aries_cloudagent/vc/ld_proofs/purposes/authentication_proof_purpose.py index 4ed8c0731a..c6d9ca140f 100644 --- a/aries_cloudagent/vc/ld_proofs/purposes/authentication_proof_purpose.py +++ b/aries_cloudagent/vc/ld_proofs/purposes/authentication_proof_purpose.py @@ -1,7 +1,7 @@ """Authentication proof purpose class.""" from datetime import datetime, timedelta -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from ..document_loader import DocumentLoaderMethod from ..error import LinkedDataProofException @@ -23,9 +23,9 @@ def __init__( self, *, challenge: str, - domain: str = None, - date: datetime = None, - max_timestamp_delta: timedelta = None, + domain: Optional[str] = None, + date: Optional[datetime] = None, + max_timestamp_delta: Optional[timedelta] = None, ): """Initialize new AuthenticationProofPurpose instance.""" super().__init__( diff --git a/aries_cloudagent/vc/ld_proofs/purposes/proof_purpose.py b/aries_cloudagent/vc/ld_proofs/purposes/proof_purpose.py index 49fc7a48c8..8bf54d8bb1 100644 --- a/aries_cloudagent/vc/ld_proofs/purposes/proof_purpose.py +++ b/aries_cloudagent/vc/ld_proofs/purposes/proof_purpose.py @@ -1,7 +1,7 @@ """Base Proof Purpose class.""" from datetime import datetime, timedelta -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from ....messaging.util import str_to_datetime @@ -17,7 +17,11 @@ class ProofPurpose: """Base proof purpose class.""" def __init__( - self, *, term: str, date: datetime = None, max_timestamp_delta: timedelta = None + self, + *, + term: str, + date: Optional[datetime] = None, + max_timestamp_delta: Optional[timedelta] = None, ): """Initialize new proof purpose instance.""" self.term = term diff --git a/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2018.py b/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2018.py index 4d72604475..d300b78f82 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2018.py +++ b/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2018.py @@ -1,7 +1,7 @@ """Ed25519Signature2018 suite.""" from datetime import datetime -from typing import Union +from typing import Optional, Union from ..crypto import _KeyPair as KeyPair @@ -17,9 +17,9 @@ def __init__( self, *, key_pair: KeyPair, - proof: dict = None, - verification_method: str = None, - date: Union[datetime, str] = None, + proof: Optional[dict] = None, + verification_method: Optional[str] = None, + date: Union[datetime, str, None] = None, ): """Create new Ed25519Signature2018 instance. diff --git a/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2020.py b/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2020.py index fee9c89084..4631b1b671 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2020.py +++ b/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2020.py @@ -1,7 +1,7 @@ """Ed25519Signature2018 suite.""" from datetime import datetime -from typing import List, Union +from typing import Optional, Union, List from ....utils.multiformats import multibase from ..crypto import _KeyPair as KeyPair @@ -19,9 +19,9 @@ def __init__( self, *, key_pair: KeyPair, - proof: dict = None, - verification_method: str = None, - date: Union[datetime, str] = None, + proof: Optional[dict] = None, + verification_method: Optional[str] = None, + date: Union[datetime, str, None] = None, ): """Create new Ed25519Signature2020 instance. diff --git a/aries_cloudagent/vc/ld_proofs/suites/jws_linked_data_signature.py b/aries_cloudagent/vc/ld_proofs/suites/jws_linked_data_signature.py index 3603dd881f..e93ffdb15d 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/jws_linked_data_signature.py +++ b/aries_cloudagent/vc/ld_proofs/suites/jws_linked_data_signature.py @@ -3,7 +3,7 @@ import json from datetime import datetime -from typing import Union +from typing import Optional, Union from pyld.jsonld import JsonLdProcessor @@ -26,9 +26,9 @@ def __init__( algorithm: str, required_key_type: str, key_pair: KeyPair, - proof: dict = None, - verification_method: str = None, - date: Union[datetime, str] = None, + proof: Optional[dict] = None, + verification_method: Optional[str] = None, + date: Union[datetime, str, None] = None, ): """Create new JwsLinkedDataSignature instance. diff --git a/aries_cloudagent/vc/vc_ld/manager.py b/aries_cloudagent/vc/vc_ld/manager.py new file mode 100644 index 0000000000..c3f2b9534d --- /dev/null +++ b/aries_cloudagent/vc/vc_ld/manager.py @@ -0,0 +1,329 @@ +"""Manager for performing Linked Data Proof signatures over JSON-LD formatted W3C VCs.""" + + +from typing import Optional + +from aries_cloudagent.vc.ld_proofs.constants import ( + SECURITY_CONTEXT_BBS_URL, + SECURITY_CONTEXT_ED25519_2020_URL, +) +from aries_cloudagent.vc.ld_proofs.document_loader import DocumentLoader + +from ...core.profile import Profile +from ...wallet.base import BaseWallet +from ...wallet.default_verification_key_strategy import BaseVerificationKeyStrategy +from ...wallet.did_info import DIDInfo +from ...wallet.error import WalletNotFoundError +from ...wallet.key_type import BLS12381G2, ED25519 +from ..ld_proofs.crypto.wallet_key_pair import WalletKeyPair +from ..ld_proofs.purposes.authentication_proof_purpose import AuthenticationProofPurpose +from ..ld_proofs.purposes.credential_issuance_purpose import CredentialIssuancePurpose +from ..ld_proofs.purposes.proof_purpose import ProofPurpose +from ..ld_proofs.suites.bbs_bls_signature_2020 import BbsBlsSignature2020 +from ..ld_proofs.suites.ed25519_signature_2018 import Ed25519Signature2018 +from ..ld_proofs.suites.ed25519_signature_2020 import Ed25519Signature2020 +from ..ld_proofs.suites.linked_data_proof import LinkedDataProof +from .issue import issue as ldp_issue +from .models.credential import VerifiableCredential +from .models.linked_data_proof import LDProof +from .models.options import LDProofVCOptions + + +SUPPORTED_ISSUANCE_PROOF_PURPOSES = { + CredentialIssuancePurpose.term, + AuthenticationProofPurpose.term, +} +SUPPORTED_ISSUANCE_SUITES = {Ed25519Signature2018, Ed25519Signature2020} +SIGNATURE_SUITE_KEY_TYPE_MAPPING = { + Ed25519Signature2018: ED25519, + Ed25519Signature2020: ED25519, +} + + +# We only want to add bbs suites to supported if the module is installed +if BbsBlsSignature2020.BBS_SUPPORTED: + SUPPORTED_ISSUANCE_SUITES.add(BbsBlsSignature2020) + SIGNATURE_SUITE_KEY_TYPE_MAPPING[BbsBlsSignature2020] = BLS12381G2 + + +PROOF_TYPE_SIGNATURE_SUITE_MAPPING = { + suite.signature_type: suite for suite in SIGNATURE_SUITE_KEY_TYPE_MAPPING +} + + +# key_type -> set of signature types mappings +KEY_TYPE_SIGNATURE_TYPE_MAPPING = { + key_type: { + suite.signature_type + for suite, kt in SIGNATURE_SUITE_KEY_TYPE_MAPPING.items() + if kt == key_type + } + for key_type in SIGNATURE_SUITE_KEY_TYPE_MAPPING.values() +} + + +class VcLdpManagerError(Exception): + """Generic VcLdpManager Error.""" + + +class VcLdpManager: + """Class for managing Linked Data Proof signatures over JSON-LD formatted W3C VCs.""" + + def __init__(self, profile: Profile): + """Initialize the VC LD Proof Manager.""" + self.profile = profile + + async def _did_info_for_did(self, did: str) -> DIDInfo: + """Get the did info for specified did. + + If the did starts with did:sov it will remove the prefix for + backwards compatibility with not fully qualified did. + + Args: + did (str): The did to retrieve from the wallet. + + Raises: + WalletNotFoundError: If the did is not found in the wallet. + + Returns: + DIDInfo: did information + + """ + async with self.profile.session() as session: + wallet = session.inject(BaseWallet) + + # If the did starts with did:sov we need to query without + if did.startswith("did:sov:"): + return await wallet.get_local_did(did.replace("did:sov:", "")) + + # All other methods we can just query + return await wallet.get_local_did(did) + + async def _assert_can_issue_with_id_and_proof_type( + self, issuer_id: str, proof_type: str + ): + """Assert that it is possible to issue using the specified id and proof type. + + Args: + issuer_id (str): The issuer id + proof_type (str): the signature suite proof type + + Raises: + VcLdpManagerError: + - If the proof type is not supported + - If the issuer id is not a did + - If the did is not found in th wallet + - If the did does not support to create signatures for the proof type + + """ + try: + # Check if it is a proof type we can issue with + if proof_type not in PROOF_TYPE_SIGNATURE_SUITE_MAPPING.keys(): + raise VcLdpManagerError( + f"Unable to sign credential with unsupported proof type {proof_type}." + f" Supported proof types: {PROOF_TYPE_SIGNATURE_SUITE_MAPPING.keys()}" + ) + + if not issuer_id.startswith("did:"): + raise VcLdpManagerError( + f"Unable to issue credential with issuer id: {issuer_id}." + " Only issuance with DIDs is supported" + ) + + # Retrieve did from wallet. Will throw if not found + did = await self._did_info_for_did(issuer_id) + + # Raise error if we cannot issue a credential with this proof type + # using this DID from + did_proof_types = KEY_TYPE_SIGNATURE_TYPE_MAPPING[did.key_type] + if proof_type not in did_proof_types: + raise VcLdpManagerError( + f"Unable to issue credential with issuer id {issuer_id} and proof " + f"type {proof_type}. DID only supports proof types {did_proof_types}" + ) + + except WalletNotFoundError: + raise VcLdpManagerError( + f"Issuer did {issuer_id} not found." + " Unable to issue credential with this DID." + ) + + async def _get_suite( + self, + *, + proof_type: str, + verification_method: Optional[str] = None, + proof: Optional[dict] = None, + did_info: Optional[DIDInfo] = None, + ): + """Get signature suite for issuance of verification.""" + session = await self.profile.session() + wallet = session.inject(BaseWallet) + + # Get signature class based on proof type + SignatureClass = PROOF_TYPE_SIGNATURE_SUITE_MAPPING[proof_type] + + # Generically create signature class + return SignatureClass( + verification_method=verification_method, + proof=proof, + key_pair=WalletKeyPair( + wallet=wallet, + key_type=SIGNATURE_SUITE_KEY_TYPE_MAPPING[SignatureClass], + public_key_base58=did_info.verkey if did_info else None, + ), + ) + + def _get_proof_purpose( + self, + *, + proof_purpose: Optional[str] = None, + challenge: Optional[str] = None, + domain: Optional[str] = None, + ) -> ProofPurpose: + """Get the proof purpose for a credential. + + Args: + proof_purpose (str): The proof purpose string value + challenge (str, optional): Challenge + domain (str, optional): domain + + Raises: + VcLdpManagerError: + - If the proof purpose is not supported. + - [authentication] If challenge is missing. + + Returns: + ProofPurpose: Proof purpose instance that can be used for issuance. + + """ + # Default proof purpose is assertionMethod + proof_purpose = proof_purpose or CredentialIssuancePurpose.term + + if proof_purpose == CredentialIssuancePurpose.term: + return CredentialIssuancePurpose() + elif proof_purpose == AuthenticationProofPurpose.term: + # assert challenge is present for authentication proof purpose + if not challenge: + raise VcLdpManagerError( + f"Challenge is required for '{proof_purpose}' proof purpose." + ) + + return AuthenticationProofPurpose(challenge=challenge, domain=domain) + else: + raise VcLdpManagerError( + f"Unsupported proof purpose: {proof_purpose}. " + f"Supported proof types are: {SUPPORTED_ISSUANCE_PROOF_PURPOSES}" + ) + + async def _prepare_credential( + self, + credential: VerifiableCredential, + options: LDProofVCOptions, + holder_did: Optional[str] = None, + ) -> VerifiableCredential: + # Add BBS context if not present yet + assert options and isinstance(options, LDProofVCOptions) + assert credential and isinstance(credential, VerifiableCredential) + if ( + options.proof_type == BbsBlsSignature2020.signature_type + and SECURITY_CONTEXT_BBS_URL not in credential.context_urls + ): + credential.add_context(SECURITY_CONTEXT_BBS_URL) + # Add ED25519-2020 context if not present yet + elif ( + options.proof_type == Ed25519Signature2020.signature_type + and SECURITY_CONTEXT_ED25519_2020_URL not in credential.context_urls + ): + credential.add_context(SECURITY_CONTEXT_ED25519_2020_URL) + + # Permit late binding of credential subject: + # IFF credential subject doesn't already have an id, add holder_did as + # credentialSubject.id (if provided) + subject = credential.credential_subject + + # TODO if credential subject is a list, we're only binding the first... + # How should this be handled? + if isinstance(subject, list): + subject = subject[0] + + if not subject: + raise VcLdpManagerError("Credential subject is required") + + if holder_did and holder_did.startswith("did:key") and "id" not in subject: + subject["id"] = holder_did + + return credential + + async def _get_suite_for_credential( + self, credential: VerifiableCredential, options: LDProofVCOptions + ) -> LinkedDataProof: + issuer_id = credential.issuer_id + proof_type = options.proof_type + + if not issuer_id: + raise VcLdpManagerError("Credential issuer id is required") + + if not proof_type: + raise VcLdpManagerError("Proof type is required") + + # Assert we can issue the credential based on issuer + proof_type + await self._assert_can_issue_with_id_and_proof_type(issuer_id, proof_type) + + # Create base proof object with options + proof = LDProof( + created=options.created, + domain=options.domain, + challenge=options.challenge, + ) + + did_info = await self._did_info_for_did(issuer_id) + verkey_id_strategy = self.profile.context.inject(BaseVerificationKeyStrategy) + verification_method = ( + options.verification_method + or await verkey_id_strategy.get_verification_method_id_for_did( + issuer_id, self.profile, proof_purpose="assertionMethod" + ) + ) + + if verification_method is None: + raise VcLdpManagerError( + f"Unable to get retrieve verification method for did {issuer_id}" + ) + + suite = await self._get_suite( + proof_type=proof_type, + verification_method=verification_method, + proof=proof.serialize(), + did_info=did_info, + ) + + return suite + + async def issue(self, credential: VerifiableCredential, options: LDProofVCOptions): + """Sign a VC with a Linked Data Proof.""" + credential = await self._prepare_credential(credential, options) + + # Get signature suite, proof purpose and document loader + suite = await self._get_suite_for_credential(credential, options) + proof_purpose = self._get_proof_purpose( + proof_purpose=options.proof_purpose, + challenge=options.challenge, + domain=options.domain, + ) + document_loader = self.profile.inject(DocumentLoader) + + # issue the credential + vc = await ldp_issue( + credential=credential.serialize(), + suite=suite, + document_loader=document_loader, + purpose=proof_purpose, + ) + return vc + + async def verify_presentation(self): + """Verify a VP with a Linked Data Proof.""" + + async def verify_credential(self): + """Verify a VC with a Linked Data Proof.""" diff --git a/aries_cloudagent/vc/vc_ld/models/options.py b/aries_cloudagent/vc/vc_ld/models/options.py new file mode 100644 index 0000000000..412b43c5a3 --- /dev/null +++ b/aries_cloudagent/vc/vc_ld/models/options.py @@ -0,0 +1,168 @@ +"""Options for specifying how the linked data proof is created.""" + + +from typing import Optional + +from marshmallow import INCLUDE, Schema, fields + +from aries_cloudagent.messaging.valid import ( + INDY_ISO8601_DATETIME_EXAMPLE, + INDY_ISO8601_DATETIME_VALIDATE, + UUID4_EXAMPLE, +) + +from ....messaging.models.base import BaseModel, BaseModelSchema + + +class LDProofVCOptions(BaseModel): + """Linked Data Proof verifiable credential options model.""" + + class Meta: + """LDProofVCDetailOptions metadata.""" + + schema_class = "LDProofVCOptionsSchema" + + def __init__( + self, + verification_method: Optional[str] = None, + proof_type: Optional[str] = None, + proof_purpose: Optional[str] = None, + created: Optional[str] = None, + domain: Optional[str] = None, + challenge: Optional[str] = None, + credential_status: Optional[dict] = None, + ) -> None: + """Initialize the LDProofVCDetailOptions instance.""" + + self.verification_method = verification_method + self.proof_type = proof_type + self.proof_purpose = proof_purpose + self.created = created + self.domain = domain + self.challenge = challenge + self.credential_status = credential_status + + def __eq__(self, o: object) -> bool: + """Check equalness.""" + if isinstance(o, LDProofVCOptions): + return ( + self.proof_type == o.proof_type + and self.proof_purpose == o.proof_purpose + and self.created == o.created + and self.domain == o.domain + and self.challenge == o.challenge + and self.credential_status == o.credential_status + ) + + return False + + +class CredentialStatusOptionsSchema(Schema): + """Linked data proof credential status options schema.""" + + class Meta: + """Accept parameter overload.""" + + unknown = INCLUDE + + type = fields.Str( + required=True, + metadata={ + "description": ( + "Credential status method type to use for the credential. Should match" + " status method registered in the Verifiable Credential Extension" + " Registry" + ), + "example": "CredentialStatusList2017", + }, + ) + + +class LDProofVCOptionsSchema(BaseModelSchema): + """Linked data proof verifiable credential options schema.""" + + class Meta: + """Accept parameter overload.""" + + unknown = INCLUDE + model_class = LDProofVCOptions + + verification_method = fields.Str( + data_key="verificationMethod", + required=True, + metadata={ + "description": ( + "The verification method to use for the proof. Should match a" + " verification method in the wallet" + ), + "example": "did:example:123456#key-1", + }, + ) + + proof_type = fields.Str( + data_key="proofType", + required=True, + metadata={ + "description": ( + "The proof type used for the proof. Should match suites registered in" + " the Linked Data Cryptographic Suite Registry" + ), + "example": "Ed25519Signature2018", + }, + ) + + proof_purpose = fields.Str( + data_key="proofPurpose", + required=False, + metadata={ + "description": ( + "The proof purpose used for the proof. Should match proof purposes" + " registered in the Linked Data Proofs Specification" + ), + "example": "assertionMethod", + }, + ) + + created = fields.Str( + required=False, + validate=INDY_ISO8601_DATETIME_VALIDATE, + metadata={ + "description": ( + "The date and time of the proof (with a maximum accuracy in seconds)." + " Defaults to current system time" + ), + "example": INDY_ISO8601_DATETIME_EXAMPLE, + }, + ) + + domain = fields.Str( + required=False, + metadata={ + "description": "The intended domain of validity for the proof", + "example": "example.com", + }, + ) + + challenge = fields.Str( + required=False, + metadata={ + "description": ( + "A challenge to include in the proof. SHOULD be provided by the" + " requesting party of the credential (=holder)" + ), + "example": UUID4_EXAMPLE, + }, + ) + + credential_status = fields.Nested( + CredentialStatusOptionsSchema(), + data_key="credentialStatus", + required=False, + metadata={ + "description": ( + "The credential status mechanism to use for the credential. Omitting" + " the property indicates the issued credential will not include a" + " credential status" + ) + }, + ) From 78c75fdbb1c98db74f1597abd8b0b8a47cbb3501 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 4 Oct 2023 09:41:23 -0400 Subject: [PATCH 02/16] fix: wallet key pair takes profile now Signed-off-by: Daniel Bluhm --- aries_cloudagent/vc/vc_ld/manager.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/aries_cloudagent/vc/vc_ld/manager.py b/aries_cloudagent/vc/vc_ld/manager.py index c3f2b9534d..c74a55914e 100644 --- a/aries_cloudagent/vc/vc_ld/manager.py +++ b/aries_cloudagent/vc/vc_ld/manager.py @@ -157,9 +157,6 @@ async def _get_suite( did_info: Optional[DIDInfo] = None, ): """Get signature suite for issuance of verification.""" - session = await self.profile.session() - wallet = session.inject(BaseWallet) - # Get signature class based on proof type SignatureClass = PROOF_TYPE_SIGNATURE_SUITE_MAPPING[proof_type] @@ -168,7 +165,7 @@ async def _get_suite( verification_method=verification_method, proof=proof, key_pair=WalletKeyPair( - wallet=wallet, + profile=self.profile, key_type=SIGNATURE_SUITE_KEY_TYPE_MAPPING[SignatureClass], public_key_base58=did_info.verkey if did_info else None, ), From aa69b6dd3606f8e57d888a06134c2cfa23bbcb8f Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 5 Oct 2023 10:52:57 -0400 Subject: [PATCH 03/16] feat: add vc ldp routes and implement verify Signed-off-by: Daniel Bluhm --- aries_cloudagent/config/default_context.py | 1 + .../purposes/authentication_proof_purpose.py | 2 +- .../purposes/controller_proof_purpose.py | 2 +- .../purposes/credential_issuance_purpose.py | 4 +- .../vc/ld_proofs/purposes/proof_purpose.py | 2 +- .../vc/ld_proofs/validation_result.py | 86 ++++++++++++++++--- aries_cloudagent/vc/vc_ld/manager.py | 44 ++++++++-- .../vc/vc_ld/validation_result.py | 33 +++++-- 8 files changed, 148 insertions(+), 26 deletions(-) diff --git a/aries_cloudagent/config/default_context.py b/aries_cloudagent/config/default_context.py index 283679c97d..18e5c4a8ca 100644 --- a/aries_cloudagent/config/default_context.py +++ b/aries_cloudagent/config/default_context.py @@ -141,6 +141,7 @@ async def load_plugins(self, context: InjectionContext): plugin_registry.register_plugin("aries_cloudagent.messaging.jsonld") plugin_registry.register_plugin("aries_cloudagent.resolver") plugin_registry.register_plugin("aries_cloudagent.settings") + plugin_registry.register_plugin("aries_cloudagent.vc") plugin_registry.register_plugin("aries_cloudagent.wallet") if wallet_type == "askar-anoncreds": plugin_registry.register_plugin("aries_cloudagent.anoncreds") diff --git a/aries_cloudagent/vc/ld_proofs/purposes/authentication_proof_purpose.py b/aries_cloudagent/vc/ld_proofs/purposes/authentication_proof_purpose.py index c6d9ca140f..1322e9712e 100644 --- a/aries_cloudagent/vc/ld_proofs/purposes/authentication_proof_purpose.py +++ b/aries_cloudagent/vc/ld_proofs/purposes/authentication_proof_purpose.py @@ -68,7 +68,7 @@ def validate( document_loader=document_loader, ) except Exception as e: - return PurposeResult(valid=False, error=e) + return PurposeResult(valid=False, error=str(e)) def update(self, proof: dict) -> dict: """Update poof purpose, challenge and domain on proof.""" diff --git a/aries_cloudagent/vc/ld_proofs/purposes/controller_proof_purpose.py b/aries_cloudagent/vc/ld_proofs/purposes/controller_proof_purpose.py index 84167348ac..8ee1306408 100644 --- a/aries_cloudagent/vc/ld_proofs/purposes/controller_proof_purpose.py +++ b/aries_cloudagent/vc/ld_proofs/purposes/controller_proof_purpose.py @@ -90,4 +90,4 @@ def validate( return result except Exception as e: - return PurposeResult(valid=False, error=e) + return PurposeResult(valid=False, error=str(e)) diff --git a/aries_cloudagent/vc/ld_proofs/purposes/credential_issuance_purpose.py b/aries_cloudagent/vc/ld_proofs/purposes/credential_issuance_purpose.py index 151857fbf2..e4150a962e 100644 --- a/aries_cloudagent/vc/ld_proofs/purposes/credential_issuance_purpose.py +++ b/aries_cloudagent/vc/ld_proofs/purposes/credential_issuance_purpose.py @@ -14,7 +14,7 @@ # Avoid circular dependency if TYPE_CHECKING: - from ..suites import LinkedDataProof + from ..suites import _LinkedDataProof as LinkedDataProof class CredentialIssuancePurpose(AssertionProofPurpose): @@ -70,4 +70,4 @@ def validate( return result except Exception as e: - return PurposeResult(valid=False, error=e) + return PurposeResult(valid=False, error=str(e)) diff --git a/aries_cloudagent/vc/ld_proofs/purposes/proof_purpose.py b/aries_cloudagent/vc/ld_proofs/purposes/proof_purpose.py index 8bf54d8bb1..1a6be5ac34 100644 --- a/aries_cloudagent/vc/ld_proofs/purposes/proof_purpose.py +++ b/aries_cloudagent/vc/ld_proofs/purposes/proof_purpose.py @@ -52,7 +52,7 @@ def validate( return PurposeResult(valid=True) except Exception as err: - return PurposeResult(valid=False, error=err) + return PurposeResult(valid=False, error=str(err)) def update(self, proof: dict) -> dict: """Update proof purpose on proof.""" diff --git a/aries_cloudagent/vc/ld_proofs/validation_result.py b/aries_cloudagent/vc/ld_proofs/validation_result.py index 40b8eaea8d..97139c2d02 100644 --- a/aries_cloudagent/vc/ld_proofs/validation_result.py +++ b/aries_cloudagent/vc/ld_proofs/validation_result.py @@ -1,13 +1,26 @@ """Proof verification and validation result classes.""" -from typing import List +from typing import Any, List, Optional +from marshmallow import fields -class PurposeResult: +from ...messaging.models.base import BaseModel, BaseModelSchema + + +class PurposeResult(BaseModel): """Proof purpose result class.""" + class Meta: + """PurposeResult metadata.""" + + schema_class = "PurposeResultSchema" + def __init__( - self, *, valid: bool, error: Exception = None, controller: dict = None + self, + *, + valid: bool, + error: Optional[str] = None, + controller: Optional[Any] = None, ) -> None: """Create new PurposeResult instance.""" self.valid = valid @@ -35,16 +48,34 @@ def __eq__(self, other: object) -> bool: return False -class ProofResult: +class PurposeResultSchema(BaseModelSchema): + """Proof purpose result schema.""" + + class Meta: + """PurposeResultSchema metadata.""" + + model_class = PurposeResult + + valid = fields.Boolean() + error = fields.Str() + controller = fields.Dict() + + +class ProofResult(BaseModel): """Proof result class.""" + class Meta: + """ProofResult metadata.""" + + schema_class = "ProofResultSchema" + def __init__( self, *, verified: bool, - proof: dict = None, - error: Exception = None, - purpose_result: PurposeResult = None, + proof: Optional[dict] = None, + error: Optional[str] = None, + purpose_result: Optional[PurposeResult] = None, ) -> None: """Create new ProofResult instance.""" self.verified = verified @@ -74,16 +105,35 @@ def __eq__(self, other: object) -> bool: return False -class DocumentVerificationResult: +class ProofResultSchema(BaseModelSchema): + """Proof result schema.""" + + class Meta: + """ProofResultSchema metadata.""" + + model_class = ProofResult + + verified = fields.Boolean() + proof = fields.Dict() + error = fields.Str() + purpose_result = fields.Nested(PurposeResultSchema) + + +class DocumentVerificationResult(BaseModel): """Domain verification result class.""" + class Meta: + """DocumentVerificationResult metadata.""" + + schema_class = "DocumentVerificationResultSchema" + def __init__( self, *, verified: bool, - document: dict = None, - results: List[ProofResult] = None, - errors: List[Exception] = None, + document: Optional[dict] = None, + results: Optional[List[ProofResult]] = None, + errors: Optional[List[str]] = None, ) -> None: """Create new DocumentVerificationResult instance.""" self.verified = verified @@ -141,3 +191,17 @@ def __eq__(self, other: object) -> bool: ) ) return False + + +class DocumentVerificationResultSchema(BaseModelSchema): + """Document verification result schema.""" + + class Meta: + """DocumentVerificationResultSchema metadata.""" + + model_class = DocumentVerificationResult + + verified = fields.Boolean(required=True) + document = fields.Dict(required=False) + results = fields.Nested(ProofResultSchema, many=True) + errors = fields.List(fields.Str(), required=False) diff --git a/aries_cloudagent/vc/vc_ld/manager.py b/aries_cloudagent/vc/vc_ld/manager.py index c74a55914e..0e7a8a2a42 100644 --- a/aries_cloudagent/vc/vc_ld/manager.py +++ b/aries_cloudagent/vc/vc_ld/manager.py @@ -7,7 +7,6 @@ SECURITY_CONTEXT_BBS_URL, SECURITY_CONTEXT_ED25519_2020_URL, ) -from aries_cloudagent.vc.ld_proofs.document_loader import DocumentLoader from ...core.profile import Profile from ...wallet.base import BaseWallet @@ -16,6 +15,7 @@ from ...wallet.error import WalletNotFoundError from ...wallet.key_type import BLS12381G2, ED25519 from ..ld_proofs.crypto.wallet_key_pair import WalletKeyPair +from ..ld_proofs.document_loader import DocumentLoader from ..ld_proofs.purposes.authentication_proof_purpose import AuthenticationProofPurpose from ..ld_proofs.purposes.credential_issuance_purpose import CredentialIssuancePurpose from ..ld_proofs.purposes.proof_purpose import ProofPurpose @@ -23,10 +23,13 @@ from ..ld_proofs.suites.ed25519_signature_2018 import Ed25519Signature2018 from ..ld_proofs.suites.ed25519_signature_2020 import Ed25519Signature2020 from ..ld_proofs.suites.linked_data_proof import LinkedDataProof +from ..ld_proofs.validation_result import DocumentVerificationResult +from ..vc_ld.validation_result import PresentationVerificationResult from .issue import issue as ldp_issue from .models.credential import VerifiableCredential from .models.linked_data_proof import LDProof from .models.options import LDProofVCOptions +from .verify import verify_credential, verify_presentation SUPPORTED_ISSUANCE_PROOF_PURPOSES = { @@ -297,7 +300,20 @@ async def _get_suite_for_credential( return suite - async def issue(self, credential: VerifiableCredential, options: LDProofVCOptions): + async def _get_all_suites(self): + """Get all supported suites for verifying presentation.""" + suites = [] + for suite, key_type in SIGNATURE_SUITE_KEY_TYPE_MAPPING.items(): + suites.append( + suite( + key_pair=WalletKeyPair(profile=self.profile, key_type=key_type), + ) + ) + return suites + + async def issue( + self, credential: VerifiableCredential, options: LDProofVCOptions + ) -> VerifiableCredential: """Sign a VC with a Linked Data Proof.""" credential = await self._prepare_credential(credential, options) @@ -317,10 +333,28 @@ async def issue(self, credential: VerifiableCredential, options: LDProofVCOption document_loader=document_loader, purpose=proof_purpose, ) - return vc + return VerifiableCredential.deserialize(vc) - async def verify_presentation(self): + async def verify_presentation( + self, vp: VerifiableCredential, options: LDProofVCOptions + ) -> PresentationVerificationResult: """Verify a VP with a Linked Data Proof.""" + if not options.challenge: + raise VcLdpManagerError("Challenge is required for verifying a VP") - async def verify_credential(self): + return await verify_presentation( + presentation=vp.serialize(), + suites=await self._get_all_suites(), + document_loader=self.profile.inject(DocumentLoader), + challenge=options.challenge, + ) + + async def verify_credential( + self, vc: VerifiableCredential + ) -> DocumentVerificationResult: """Verify a VC with a Linked Data Proof.""" + return await verify_credential( + credential=vc.serialize(), + suites=await self._get_all_suites(), + document_loader=self.profile.inject(DocumentLoader), + ) diff --git a/aries_cloudagent/vc/vc_ld/validation_result.py b/aries_cloudagent/vc/vc_ld/validation_result.py index 45d9e76f41..bde3b2edf2 100644 --- a/aries_cloudagent/vc/vc_ld/validation_result.py +++ b/aries_cloudagent/vc/vc_ld/validation_result.py @@ -1,20 +1,29 @@ """Presentation verification and validation result classes.""" -from typing import List +from typing import List, Optional +from marshmallow import fields + +from ...messaging.models.base import BaseModel, BaseModelSchema +from ...vc.ld_proofs.validation_result import DocumentVerificationResultSchema from ..ld_proofs import DocumentVerificationResult -class PresentationVerificationResult: +class PresentationVerificationResult(BaseModel): """Presentation verification result class.""" + class Meta: + """PresentationVerificationResult metadata.""" + + schema_class = "PresentationVerificationResultSchema" + def __init__( self, *, verified: bool, - presentation_result: DocumentVerificationResult = None, - credential_results: List[DocumentVerificationResult] = None, - errors: List[Exception] = None, + presentation_result: Optional[DocumentVerificationResult] = None, + credential_results: Optional[List[DocumentVerificationResult]] = None, + errors: Optional[List[str]] = None, ) -> None: """Create new PresentationVerificationResult instance.""" self.verified = verified @@ -72,3 +81,17 @@ def __eq__(self, other: object) -> bool: ) ) return False + + +class PresentationVerificationResultSchema(BaseModelSchema): + """Presentation verification result schema.""" + + class Meta: + """PresentationVerificationResultSchema metadata.""" + + model_class = PresentationVerificationResult + + verified = fields.Bool(required=True) + presentation_result = fields.Nested(DocumentVerificationResultSchema) + credential_results = fields.List(fields.Nested(DocumentVerificationResultSchema)) + errors = fields.List(fields.Str()) From f7d27fe49368270e55c8861ca211d8a10e17e134 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 5 Oct 2023 10:55:32 -0400 Subject: [PATCH 04/16] feat: mark jsonld sign and verify as deprecated The `/jsonld/sign` and `/jsonld/verify` endpoints will be removed in a future version of ACA-Py. Signed-off-by: Daniel Bluhm --- aries_cloudagent/messaging/jsonld/routes.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/messaging/jsonld/routes.py b/aries_cloudagent/messaging/jsonld/routes.py index b7330f9d83..12c8105571 100644 --- a/aries_cloudagent/messaging/jsonld/routes.py +++ b/aries_cloudagent/messaging/jsonld/routes.py @@ -59,7 +59,11 @@ class SignResponseSchema(OpenAPISchema): error = fields.Str(required=False, metadata={"description": "Error text"}) -@docs(tags=["jsonld"], summary="Sign a JSON-LD structure and return it") +@docs( + tags=["jsonld"], + summary="Sign a JSON-LD structure and return it", + deprecated=True, +) @request_schema(SignRequestSchema()) @response_schema(SignResponseSchema(), 200, description="") async def sign(request: web.BaseRequest): @@ -119,7 +123,11 @@ class VerifyResponseSchema(OpenAPISchema): error = fields.Str(required=False, metadata={"description": "Error text"}) -@docs(tags=["jsonld"], summary="Verify a JSON-LD structure.") +@docs( + tags=["jsonld"], + summary="Verify a JSON-LD structure.", + deprecated=True, +) @request_schema(VerifyRequestSchema()) @response_schema(VerifyResponseSchema(), 200, description="") async def verify(request: web.BaseRequest): From b083e513241b219add2b5d774d0a55fdc713e04e Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 5 Oct 2023 14:50:58 -0400 Subject: [PATCH 05/16] feat: integrate vc ldp manager in ICv2, PPv2 Signed-off-by: Daniel Bluhm --- .../v2_0/formats/ld_proof/handler.py | 364 +++--------------- .../protocols/present_proof/dif/pres_exch.py | 52 +-- .../present_proof/dif/tests/test_pres_exch.py | 6 +- .../present_proof/v2_0/formats/dif/handler.py | 83 ++-- aries_cloudagent/vc/routes.py | 149 +++++++ aries_cloudagent/vc/vc_ld/manager.py | 22 +- .../vc/vc_ld/models/presentation.py | 63 +++ 7 files changed, 323 insertions(+), 416 deletions(-) create mode 100644 aries_cloudagent/vc/routes.py create mode 100644 aries_cloudagent/vc/vc_ld/models/presentation.py diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/handler.py index 14ce9ac582..da76e9dd8c 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/handler.py @@ -2,7 +2,7 @@ import logging -from typing import Mapping, Optional +from typing import Mapping from marshmallow import EXCLUDE, INCLUDE from pyld import jsonld @@ -11,30 +11,12 @@ from ......messaging.decorators.attach_decorator import AttachDecorator from ......storage.vc_holder.base import VCHolder from ......storage.vc_holder.vc_record import VCRecord -from ......vc.ld_proofs import ( - AuthenticationProofPurpose, - BbsBlsSignature2020, - CredentialIssuancePurpose, - DocumentLoader, - Ed25519Signature2018, - Ed25519Signature2020, - LinkedDataProof, - ProofPurpose, - WalletKeyPair, -) +from ......vc.ld_proofs import DocumentLoader from ......vc.ld_proofs.check import get_properties_without_context -from ......vc.ld_proofs.constants import ( - SECURITY_CONTEXT_BBS_URL, - SECURITY_CONTEXT_ED25519_2020_URL, -) from ......vc.ld_proofs.error import LinkedDataProofException -from ......vc.vc_ld import LDProof, VerifiableCredential, VerifiableCredentialSchema -from ......vc.vc_ld import issue_vc as issue -from ......vc.vc_ld import verify_credential -from ......wallet.base import BaseWallet, DIDInfo -from ......wallet.default_verification_key_strategy import BaseVerificationKeyStrategy -from ......wallet.error import WalletNotFoundError -from ......wallet.key_type import BLS12381G2, ED25519 +from ......vc.vc_ld import VerifiableCredential, VerifiableCredentialSchema +from ......vc.vc_ld.manager import VcLdpManager, VcLdpManagerError +from ......vc.vc_ld.models.options import LDProofVCOptions from ...message_types import ( ATTACHMENT_FORMAT, CRED_20_ISSUE, @@ -50,43 +32,10 @@ from ...models.cred_ex_record import V20CredExRecord from ...models.detail.ld_proof import V20CredExRecordLDProof from ..handler import CredFormatAttachment, V20CredFormatError, V20CredFormatHandler -from .models.cred_detail_options import LDProofVCDetailOptions from .models.cred_detail import LDProofVCDetail, LDProofVCDetailSchema LOGGER = logging.getLogger(__name__) -SUPPORTED_ISSUANCE_PROOF_PURPOSES = { - CredentialIssuancePurpose.term, - AuthenticationProofPurpose.term, -} -SUPPORTED_ISSUANCE_SUITES = {Ed25519Signature2018, Ed25519Signature2020} -SIGNATURE_SUITE_KEY_TYPE_MAPPING = { - Ed25519Signature2018: ED25519, - Ed25519Signature2020: ED25519, -} - - -# We only want to add bbs suites to supported if the module is installed -if BbsBlsSignature2020.BBS_SUPPORTED: - SUPPORTED_ISSUANCE_SUITES.add(BbsBlsSignature2020) - SIGNATURE_SUITE_KEY_TYPE_MAPPING[BbsBlsSignature2020] = BLS12381G2 - - -PROOF_TYPE_SIGNATURE_SUITE_MAPPING = { - suite.signature_type: suite for suite in SIGNATURE_SUITE_KEY_TYPE_MAPPING -} - - -# key_type -> set of signature types mappings -KEY_TYPE_SIGNATURE_TYPE_MAPPING = { - key_type: { - suite.signature_type - for suite, kt in SIGNATURE_SUITE_KEY_TYPE_MAPPING.items() - if kt == key_type - } - for key_type in SIGNATURE_SUITE_KEY_TYPE_MAPPING.values() -} - class LDProofCredFormatHandler(V20CredFormatHandler): """Linked data proof credential format handler.""" @@ -180,223 +129,20 @@ def get_format_data(self, message_type: str, data: dict) -> CredFormatAttachment ), ) - async def _assert_can_issue_with_id_and_proof_type( - self, issuer_id: str, proof_type: str - ): - """Assert that it is possible to issue using the specified id and proof type. - - Args: - issuer_id (str): The issuer id - proof_type (str): the signature suite proof type - - Raises: - V20CredFormatError: - - If the proof type is not supported - - If the issuer id is not a did - - If the did is not found in th wallet - - If the did does not support to create signatures for the proof type - - """ - try: - # Check if it is a proof type we can issue with - if proof_type not in PROOF_TYPE_SIGNATURE_SUITE_MAPPING.keys(): - raise V20CredFormatError( - f"Unable to sign credential with unsupported proof type {proof_type}." - f" Supported proof types: {PROOF_TYPE_SIGNATURE_SUITE_MAPPING.keys()}" - ) - - if not issuer_id.startswith("did:"): - raise V20CredFormatError( - f"Unable to issue credential with issuer id: {issuer_id}." - " Only issuance with DIDs is supported" - ) - - # Retrieve did from wallet. Will throw if not found - did = await self._did_info_for_did(issuer_id) - - # Raise error if we cannot issue a credential with this proof type - # using this DID from - did_proof_types = KEY_TYPE_SIGNATURE_TYPE_MAPPING[did.key_type] - if proof_type not in did_proof_types: - raise V20CredFormatError( - f"Unable to issue credential with issuer id {issuer_id} and proof " - f"type {proof_type}. DID only supports proof types {did_proof_types}" - ) - - except WalletNotFoundError: - raise V20CredFormatError( - f"Issuer did {issuer_id} not found." - " Unable to issue credential with this DID." - ) - - async def _did_info_for_did(self, did: str) -> DIDInfo: - """Get the did info for specified did. - - If the did starts with did:sov it will remove the prefix for - backwards compatibility with not fully qualified did. - - Args: - did (str): The did to retrieve from the wallet. - - Raises: - WalletNotFoundError: If the did is not found in the wallet. - - Returns: - DIDInfo: did information - - """ - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - - # If the did starts with did:sov we need to query without - if did.startswith("did:sov:"): - return await wallet.get_local_did(did.replace("did:sov:", "")) - - # All other methods we can just query - return await wallet.get_local_did(did) - - async def _get_suite_for_detail( - self, detail: LDProofVCDetail, verification_method: Optional[str] = None - ) -> LinkedDataProof: - issuer_id = detail.credential.issuer_id - proof_type = detail.options.proof_type - - # Assert we can issue the credential based on issuer + proof_type - await self._assert_can_issue_with_id_and_proof_type(issuer_id, proof_type) - - # Create base proof object with options from detail - proof = LDProof( - created=detail.options.created, - domain=detail.options.domain, - challenge=detail.options.challenge, - ) - - did_info = await self._did_info_for_did(issuer_id) - verkey_id_strategy = self.profile.context.inject(BaseVerificationKeyStrategy) - verification_method = ( - verification_method - or await verkey_id_strategy.get_verification_method_id_for_did( - issuer_id, self.profile, proof_purpose="assertionMethod" - ) - ) - - if verification_method is None: - raise V20CredFormatError( - f"Unable to get retrieve verification method for did {issuer_id}" - ) - - suite = await self._get_suite( - proof_type=proof_type, - verification_method=verification_method, - proof=proof.serialize(), - did_info=did_info, - ) - - return suite - - async def _get_suite( - self, - *, - proof_type: str, - verification_method: str = None, - proof: dict = None, - did_info: DIDInfo = None, - ): - """Get signature suite for issuance of verification.""" - # Get signature class based on proof type - SignatureClass = PROOF_TYPE_SIGNATURE_SUITE_MAPPING[proof_type] - - # Generically create signature class - return SignatureClass( - verification_method=verification_method, - proof=proof, - key_pair=WalletKeyPair( - profile=self.profile, - key_type=SIGNATURE_SUITE_KEY_TYPE_MAPPING[SignatureClass], - public_key_base58=did_info.verkey if did_info else None, - ), - ) - - def _get_proof_purpose( - self, *, proof_purpose: str = None, challenge: str = None, domain: str = None - ) -> ProofPurpose: - """Get the proof purpose for a credential detail. - - Args: - proof_purpose (str): The proof purpose string value - challenge (str, optional): Challenge - domain (str, optional): domain - - Raises: - V20CredFormatError: - - If the proof purpose is not supported. - - [authentication] If challenge is missing. - - Returns: - ProofPurpose: Proof purpose instance that can be used for issuance. - - """ - # Default proof purpose is assertionMethod - proof_purpose = proof_purpose or CredentialIssuancePurpose.term - - if proof_purpose == CredentialIssuancePurpose.term: - return CredentialIssuancePurpose() - elif proof_purpose == AuthenticationProofPurpose.term: - # assert challenge is present for authentication proof purpose - if not challenge: - raise V20CredFormatError( - f"Challenge is required for '{proof_purpose}' proof purpose." - ) - - return AuthenticationProofPurpose(challenge=challenge, domain=domain) - else: - raise V20CredFormatError( - f"Unsupported proof purpose: {proof_purpose}. " - f"Supported proof types are: {SUPPORTED_ISSUANCE_PROOF_PURPOSES}" - ) - - async def _prepare_detail( - self, detail: LDProofVCDetail, holder_did: str = None - ) -> LDProofVCDetail: - # Add BBS context if not present yet - assert detail.options and isinstance(detail.options, LDProofVCDetailOptions) - assert detail.credential and isinstance(detail.credential, VerifiableCredential) - if ( - detail.options.proof_type == BbsBlsSignature2020.signature_type - and SECURITY_CONTEXT_BBS_URL not in detail.credential.context_urls - ): - detail.credential.add_context(SECURITY_CONTEXT_BBS_URL) - # Add ED25519-2020 context if not present yet - elif ( - detail.options.proof_type == Ed25519Signature2020.signature_type - and SECURITY_CONTEXT_ED25519_2020_URL not in detail.credential.context_urls - ): - detail.credential.add_context(SECURITY_CONTEXT_ED25519_2020_URL) - - # Permit late binding of credential subject: - # IFF credential subject doesn't already have an id, add holder_did as - # credentialSubject.id (if provided) - subject = detail.credential.credential_subject - - # TODO if credential subject is a list, we're only binding the first... - # How should this be handled? - if isinstance(subject, list): - subject = subject[0] - - if not subject: - raise V20CredFormatError("Credential subject is required") - - if holder_did and holder_did.startswith("did:key") and "id" not in subject: - subject["id"] = holder_did - - return detail - async def create_proposal( self, cred_ex_record: V20CredExRecord, proposal_data: Mapping ) -> CredFormatAttachment: """Create linked data proof credential proposal.""" + manager = VcLdpManager(self.profile) detail = LDProofVCDetail.deserialize(proposal_data) - detail = await self._prepare_detail(detail) + assert detail.options and isinstance(detail.options, LDProofVCOptions) + assert detail.credential and isinstance(detail.credential, VerifiableCredential) + try: + detail.credential = await manager.prepare_credential( + detail.credential, detail.options + ) + except VcLdpManagerError as err: + raise V20CredFormatError("Failed to prepare credential") from err return self.get_format_data(CRED_20_PROPOSAL, detail.serialize()) @@ -419,7 +165,15 @@ async def create_offer( # but also when we create an offer (manager does some weird stuff) offer_data = cred_proposal_message.attachment(LDProofCredFormatHandler.format) detail = LDProofVCDetail.deserialize(offer_data) - detail = await self._prepare_detail(detail) + manager = VcLdpManager(self.profile) + assert detail.options and isinstance(detail.options, LDProofVCOptions) + assert detail.credential and isinstance(detail.credential, VerifiableCredential) + try: + detail.credential = await manager.prepare_credential( + detail.credential, detail.options + ) + except VcLdpManagerError as err: + raise V20CredFormatError("Failed to prepare credential") from err document_loader = self.profile.inject(DocumentLoader) missing_properties = get_properties_without_context( @@ -433,9 +187,14 @@ async def create_offer( ) # Make sure we can issue with the did and proof type - await self._assert_can_issue_with_id_and_proof_type( - detail.credential.issuer_id, detail.options.proof_type - ) + try: + await manager.assert_can_issue_with_id_and_proof_type( + detail.credential.issuer_id, detail.options.proof_type + ) + except VcLdpManagerError as err: + raise V20CredFormatError( + "Checking whether issuance is possible failed" + ) from err return self.get_format_data(CRED_20_OFFER, detail.serialize()) @@ -466,7 +225,15 @@ async def create_request( ) detail = LDProofVCDetail.deserialize(request_data) - detail = await self._prepare_detail(detail, holder_did=holder_did) + manager = VcLdpManager(self.profile) + assert detail.options and isinstance(detail.options, LDProofVCOptions) + assert detail.credential and isinstance(detail.credential, VerifiableCredential) + try: + detail.credential = await manager.prepare_credential( + detail.credential, detail.options, holder_did=holder_did + ) + except VcLdpManagerError as err: + raise V20CredFormatError("Failed to prepare credential") from err return self.get_format_data(CRED_20_REQUEST, detail.serialize()) @@ -524,28 +291,15 @@ async def issue_credential( LDProofCredFormatHandler.format ) detail = LDProofVCDetail.deserialize(detail_dict) - detail = await self._prepare_detail(detail) - - # Get signature suite, proof purpose and document loader - suite = await self._get_suite_for_detail( - detail, cred_ex_record.verification_method - ) - proof_purpose = self._get_proof_purpose( - proof_purpose=detail.options.proof_purpose, - challenge=detail.options.challenge, - domain=detail.options.domain, - ) - document_loader = self.profile.inject(DocumentLoader) - - # issue the credential - vc = await issue( - credential=detail.credential.serialize(), - suite=suite, - document_loader=document_loader, - purpose=proof_purpose, - ) + manager = VcLdpManager(self.profile) + assert detail.options and isinstance(detail.options, LDProofVCOptions) + assert detail.credential and isinstance(detail.credential, VerifiableCredential) + try: + vc = await manager.issue(detail.credential, detail.options) + except VcLdpManagerError as err: + raise V20CredFormatError("Failed to issue credential") from err - return self.get_format_data(CRED_20_ISSUE, vc) + return self.get_format_data(CRED_20_ISSUE, vc.serialize()) async def receive_credential( self, cred_ex_record: V20CredExRecord, cred_issue_message: V20CredIssue @@ -628,27 +382,17 @@ async def store_credential( credential = VerifiableCredential.deserialize(cred_dict, unknown=INCLUDE) # Get signature suite, proof purpose and document loader - suite = await self._get_suite(proof_type=credential.proof.type) - - purpose = self._get_proof_purpose( - proof_purpose=credential.proof.proof_purpose, - challenge=credential.proof.challenge, - domain=credential.proof.domain, - ) - document_loader = self.profile.inject(DocumentLoader) - - # Verify the credential - result = await verify_credential( - credential=cred_dict, - suites=[suite], - document_loader=document_loader, - purpose=purpose, - ) + manager = VcLdpManager(self.profile) + try: + result = await manager.verify_credential(credential) + except VcLdpManagerError as err: + raise V20CredFormatError("Failed to verify credential") from err if not result.verified: raise V20CredFormatError(f"Received invalid credential: {result}") # Saving expanded type as a cred_tag + document_loader = self.profile.inject(DocumentLoader) expanded = jsonld.expand(cred_dict, options={"documentLoader": document_loader}) types = JsonLdProcessor.get_values( expanded[0], diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py index c7f974ddb1..7018da2d78 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py @@ -1,5 +1,5 @@ """Schemas for dif presentation exchange attachment.""" -from typing import Mapping, Sequence, Union +from typing import Mapping, Optional, Sequence from marshmallow import ( EXCLUDE, @@ -12,13 +12,11 @@ ) from ....messaging.models.base import BaseModel, BaseModelSchema -from ....messaging.valid import ( - UUID4_EXAMPLE, - UUID4_VALIDATE, - StrOrDictField, - StrOrNumberField, +from ....messaging.valid import StrOrNumberField, UUID4_EXAMPLE, UUID4_VALIDATE +from ....vc.vc_ld.models.presentation import ( + VerifiablePresentation, + VerifiablePresentationSchema, ) -from ....vc.vc_ld import LinkedDataProofSchema class ClaimFormat(BaseModel): @@ -840,60 +838,34 @@ class Meta: ) -class VerifiablePresentation(BaseModel): +class VPWithSubmission(VerifiablePresentation): """Single VerifiablePresentation object.""" class Meta: """VerifiablePresentation metadata.""" - schema_class = "VerifiablePresentationSchema" + schema_class = "VPWithSubmissionSchema" def __init__( self, *, - id: str = None, - contexts: Sequence[Union[str, dict]] = None, - types: Sequence[str] = None, - credentials: Sequence[dict] = None, - proof: Sequence[dict] = None, - presentation_submission: PresentationSubmission = None, + presentation_submission: Optional[PresentationSubmission] = None, + **kwargs, ): """Initialize VerifiablePresentation.""" - self.id = id - self.contexts = contexts - self.types = types - self.credentials = credentials - self.proof = proof + super().__init__(**kwargs) self.presentation_submission = presentation_submission -class VerifiablePresentationSchema(BaseModelSchema): +class VPWithSubmissionSchema(VerifiablePresentationSchema): """Single Verifiable Presentation Schema.""" class Meta: """VerifiablePresentationSchema metadata.""" - model_class = VerifiablePresentation + model_class = VPWithSubmission unknown = INCLUDE - id = fields.Str( - required=False, - validate=UUID4_VALIDATE, - metadata={"description": "ID", "example": UUID4_EXAMPLE}, - ) - contexts = fields.List(StrOrDictField(), data_key="@context") - types = fields.List( - fields.Str(required=False, metadata={"description": "Types"}), data_key="type" - ) - credentials = fields.List( - fields.Dict(required=False, metadata={"description": "Credentials"}), - data_key="verifiableCredential", - ) - proof = fields.Nested( - LinkedDataProofSchema(), - required=True, - metadata={"description": "The proof of the credential"}, - ) presentation_submission = fields.Nested(PresentationSubmissionSchema) diff --git a/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch.py b/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch.py index b8515188cd..6544e88d88 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch.py @@ -11,7 +11,7 @@ DIFHolder, Filter, Constraints, - VerifiablePresentation, + VPWithSubmission, SchemasInputDescriptorFilter, ) @@ -377,8 +377,8 @@ def test_verifiable_presentation_wrapper(self): "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..2uBYmg7muE9ZPVeAGo_ibVfLkCjf2hGshr2o5i8pAwFyNBM-kDHXofuq1MzJgb19wzb01VIu91hY_ajjt9KFAA", }, } - vp = VerifiablePresentation.deserialize(test_vp_dict) - assert isinstance(vp, VerifiablePresentation) + vp = VPWithSubmission.deserialize(test_vp_dict) + assert isinstance(vp, VPWithSubmission) def test_schemas_input_desc_filter(self): test_schema_list = [ diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py index db0d824205..df47a14357 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py @@ -2,10 +2,10 @@ import json import logging +from typing import Mapping, Sequence, Tuple +from uuid import uuid4 from marshmallow import RAISE -from typing import Mapping, Tuple, Sequence -from uuid import uuid4 from ......messaging.base_handler import BaseResponder from ......messaging.decorators.attach_decorator import AttachDecorator @@ -13,38 +13,29 @@ from ......storage.vc_holder.base import VCHolder from ......storage.vc_holder.vc_record import VCRecord from ......vc.ld_proofs import ( - DocumentLoader, + BbsBlsSignature2020, Ed25519Signature2018, Ed25519Signature2020, - BbsBlsSignature2020, - BbsBlsSignatureProof2020, - WalletKeyPair, ) -from ......vc.vc_ld.verify import verify_presentation -from ......wallet.key_type import ED25519, BLS12381G2 - +from ......vc.vc_ld.manager import VcLdpManager +from ......vc.vc_ld.models.options import LDProofVCOptions +from ......vc.vc_ld.models.presentation import VerifiablePresentation from .....problem_report.v1_0.message import ProblemReport - from ....dif.pres_exch import PresentationDefinition, SchemaInputDescriptor -from ....dif.pres_exch_handler import DIFPresExchHandler, DIFPresExchError +from ....dif.pres_exch_handler import DIFPresExchError, DIFPresExchHandler from ....dif.pres_proposal_schema import DIFProofProposalSchema -from ....dif.pres_request_schema import ( - DIFProofRequestSchema, - DIFPresSpecSchema, -) +from ....dif.pres_request_schema import DIFPresSpecSchema, DIFProofRequestSchema from ....dif.pres_schema import DIFProofSchema from ....v2_0.messages.pres_problem_report import ProblemReportReason - from ...message_types import ( ATTACHMENT_FORMAT, - PRES_20_REQUEST, PRES_20, PRES_20_PROPOSAL, + PRES_20_REQUEST, ) -from ...messages.pres_format import V20PresFormat from ...messages.pres import V20Pres +from ...messages.pres_format import V20PresFormat from ...models.pres_exchange import V20PresExRecord - from ..handler import V20PresFormatHandler, V20PresFormatHandlerError LOGGER = logging.getLogger(__name__) @@ -55,26 +46,6 @@ class DIFPresFormatHandler(V20PresFormatHandler): format = V20PresFormat.Format.DIF - ISSUE_SIGNATURE_SUITE_KEY_TYPE_MAPPING = { - Ed25519Signature2018: ED25519, - Ed25519Signature2020: ED25519, - } - - if BbsBlsSignature2020.BBS_SUPPORTED: - ISSUE_SIGNATURE_SUITE_KEY_TYPE_MAPPING[BbsBlsSignature2020] = BLS12381G2 - ISSUE_SIGNATURE_SUITE_KEY_TYPE_MAPPING[BbsBlsSignatureProof2020] = BLS12381G2 - - async def _get_all_suites(self): - """Get all supported suites for verifying presentation.""" - suites = [] - for suite, key_type in self.ISSUE_SIGNATURE_SUITE_KEY_TYPE_MAPPING.items(): - suites.append( - suite( - key_pair=WalletKeyPair(profile=self._profile, key_type=key_type), - ) - ) - return suites - @classmethod def validate_fields(cls, message_type: str, attachment_data: Mapping): """Validate attachment data for a specific message type. @@ -474,27 +445,31 @@ async def verify_pres(self, pres_ex_record: V20PresExRecord) -> V20PresExRecord: pres_request = pres_ex_record.pres_request.attachment( DIFPresFormatHandler.format ) - challenge = None - if "options" in pres_request: - challenge = pres_request["options"].get("challenge", str(uuid4())) - if not challenge: - challenge = str(uuid4()) + manager = VcLdpManager(self._profile) + + options = LDProofVCOptions.deserialize(pres_request["options"]) + if not options.challenge: + options.challenge = str(uuid4()) + + pres_ver_result = None if isinstance(dif_proof, Sequence): + if len(dif_proof) == 0: + raise V20PresFormatHandlerError( + "Presentation exchange record has no presentations to verify" + ) for proof in dif_proof: - pres_ver_result = await verify_presentation( - presentation=proof, - suites=await self._get_all_suites(), - document_loader=self._profile.inject(DocumentLoader), - challenge=challenge, + pres_ver_result = await manager.verify_presentation( + vp=VerifiablePresentation.deserialize(proof), + options=options, ) if not pres_ver_result.verified: break else: - pres_ver_result = await verify_presentation( - presentation=dif_proof, - suites=await self._get_all_suites(), - document_loader=self._profile.inject(DocumentLoader), - challenge=challenge, + pres_ver_result = await manager.verify_presentation( + vp=VerifiablePresentation.deserialize(dif_proof), + options=options, ) + + assert pres_ver_result is not None pres_ex_record.verified = json.dumps(pres_ver_result.verified) return pres_ex_record diff --git a/aries_cloudagent/vc/routes.py b/aries_cloudagent/vc/routes.py new file mode 100644 index 0000000000..1a4031a936 --- /dev/null +++ b/aries_cloudagent/vc/routes.py @@ -0,0 +1,149 @@ +"""VC Routes.""" + +from aiohttp import web +from aiohttp_apispec import docs, request_schema, response_schema + +from marshmallow import ValidationError, fields, validates_schema + +from aries_cloudagent.vc.vc_ld.validation_result import ( + PresentationVerificationResultSchema, +) + +from .vc_ld.models.credential import ( + CredentialSchema, + VerifiableCredential, + VerifiableCredentialSchema, +) +from .vc_ld.models.options import LDProofVCOptions, LDProofVCOptionsSchema +from .vc_ld.manager import VcLdpManager, VcLdpManagerError +from ..admin.request_context import AdminRequestContext +from ..config.base import InjectionError +from ..resolver.base import ResolverError +from ..wallet.error import WalletError +from ..messaging.models.openapi import OpenAPISchema + + +class LdpIssueRequestSchema(OpenAPISchema): + """Request schema for signing an ldb_vc.""" + + credential = fields.Nested(CredentialSchema) + options = fields.Nested(LDProofVCOptionsSchema) + + +class LdpIssueResponseSchema(OpenAPISchema): + """Request schema for signing an ldb_vc.""" + + vc = fields.Nested(VerifiableCredentialSchema) + + +@docs(tags=["ldp_vc"], summary="Sign an LDP VC.") +@request_schema(LdpIssueRequestSchema()) +@response_schema(LdpIssueResponseSchema(), 200, description="") +async def ldp_issue(request: web.BaseRequest): + """Request handler for signing a jsonld doc. + + Args: + request: aiohttp request object + + """ + context: AdminRequestContext = request["context"] + body = await request.json() + credential = VerifiableCredential.deserialize(body["credential"]) + options = LDProofVCOptions.deserialize(body["options"]) + + try: + manager = VcLdpManager(context.profile) + vc = await manager.issue(credential, options) + except VcLdpManagerError as err: + return web.json_response({"error": str(err)}, status=400) + except (WalletError, InjectionError): + raise web.HTTPForbidden(reason="No wallet available") + return web.json_response({"vc": vc.serialize()}) + + +class LdpVerifyRequestSchema(OpenAPISchema): + """Request schema for verifying an LDP VP.""" + + vp = fields.Nested(VerifiableCredentialSchema, required=False) + vc = fields.Nested(VerifiableCredentialSchema, required=False) + options = fields.Nested(LDProofVCOptionsSchema) + + @validates_schema + def validate_fields(self, data, **kwargs): + """Validate schema fields. + + Args: + data: The data to validate + + Raises: + ValidationError: if data has neither indy nor ld_proof + + """ + if not data.get("vp") and not data.get("vc"): + raise ValidationError("Field vp or vc must be present") + if data.get("vp") and data.get("vc"): + raise ValidationError("Field vp or vc must be present, not both") + + +class LdpVerifyResponseSchema(PresentationVerificationResultSchema): + """Request schema for verifying an LDP VP.""" + + +@docs(tags=["ldp_vc"], summary="Verify an LDP VC or VP.") +@request_schema(LdpVerifyRequestSchema()) +@response_schema(LdpVerifyResponseSchema(), 200, description="") +async def ldp_verify(request: web.BaseRequest): + """Request handler for signing a jsonld doc. + + Args: + request: aiohttp request object + + """ + context: AdminRequestContext = request["context"] + body = await request.json() + vp = body.get("vp") + vc = body.get("vc") + try: + manager = VcLdpManager(context.profile) + if vp: + vp = VerifiableCredential.deserialize(vp) + options = LDProofVCOptions.deserialize(body["options"]) + result = await manager.verify_presentation(vp, options) + elif vc: + vc = VerifiableCredential.deserialize(vc) + result = await manager.verify_credential(vc) + else: + raise web.HTTPBadRequest(reason="vp or vc must be present") + return web.json_response(result.serialize()) + except (VcLdpManagerError, ResolverError, ValueError) as error: + raise web.HTTPBadRequest(reason=str(error)) + except (WalletError, InjectionError): + raise web.HTTPForbidden(reason="No wallet available") + + +async def register(app: web.Application): + """Register routes.""" + + app.add_routes( + [ + web.post("/vc/ldp/issue", ldp_issue), + web.post("/vc/ldp/verify", ldp_verify), + ] + ) + + +def post_process_routes(app: web.Application): + """Amend swagger API.""" + # Add top-level tags description + if "tags" not in app._state["swagger_dict"]: + app._state["swagger_dict"]["tags"] = [] + app._state["swagger_dict"]["tags"].append( + { + "name": "jsonld", + "description": "Sign and verify json-ld data", + "externalDocs": { + "description": "Specification", + "url": "https://tools.ietf.org/html/rfc7515", + }, + } + ) diff --git a/aries_cloudagent/vc/vc_ld/manager.py b/aries_cloudagent/vc/vc_ld/manager.py index 0e7a8a2a42..27b7fbf39f 100644 --- a/aries_cloudagent/vc/vc_ld/manager.py +++ b/aries_cloudagent/vc/vc_ld/manager.py @@ -9,6 +9,7 @@ ) from ...core.profile import Profile +from ..vc_ld.models.presentation import VerifiablePresentation from ...wallet.base import BaseWallet from ...wallet.default_verification_key_strategy import BaseVerificationKeyStrategy from ...wallet.did_info import DIDInfo @@ -102,8 +103,8 @@ async def _did_info_for_did(self, did: str) -> DIDInfo: # All other methods we can just query return await wallet.get_local_did(did) - async def _assert_can_issue_with_id_and_proof_type( - self, issuer_id: str, proof_type: str + async def assert_can_issue_with_id_and_proof_type( + self, issuer_id: Optional[str], proof_type: Optional[str] ): """Assert that it is possible to issue using the specified id and proof type. @@ -119,6 +120,11 @@ async def _assert_can_issue_with_id_and_proof_type( - If the did does not support to create signatures for the proof type """ + if not issuer_id or not proof_type: + raise VcLdpManagerError( + "Issuer id and proof type are required to issue a credential." + ) + try: # Check if it is a proof type we can issue with if proof_type not in PROOF_TYPE_SIGNATURE_SUITE_MAPPING.keys(): @@ -216,15 +222,14 @@ def _get_proof_purpose( f"Supported proof types are: {SUPPORTED_ISSUANCE_PROOF_PURPOSES}" ) - async def _prepare_credential( + async def prepare_credential( self, credential: VerifiableCredential, options: LDProofVCOptions, holder_did: Optional[str] = None, ) -> VerifiableCredential: + """Prepare a credential for issuance.""" # Add BBS context if not present yet - assert options and isinstance(options, LDProofVCOptions) - assert credential and isinstance(credential, VerifiableCredential) if ( options.proof_type == BbsBlsSignature2020.signature_type and SECURITY_CONTEXT_BBS_URL not in credential.context_urls @@ -268,7 +273,7 @@ async def _get_suite_for_credential( raise VcLdpManagerError("Proof type is required") # Assert we can issue the credential based on issuer + proof_type - await self._assert_can_issue_with_id_and_proof_type(issuer_id, proof_type) + await self.assert_can_issue_with_id_and_proof_type(issuer_id, proof_type) # Create base proof object with options proof = LDProof( @@ -315,7 +320,7 @@ async def issue( self, credential: VerifiableCredential, options: LDProofVCOptions ) -> VerifiableCredential: """Sign a VC with a Linked Data Proof.""" - credential = await self._prepare_credential(credential, options) + credential = await self.prepare_credential(credential, options) # Get signature suite, proof purpose and document loader suite = await self._get_suite_for_credential(credential, options) @@ -326,7 +331,6 @@ async def issue( ) document_loader = self.profile.inject(DocumentLoader) - # issue the credential vc = await ldp_issue( credential=credential.serialize(), suite=suite, @@ -336,7 +340,7 @@ async def issue( return VerifiableCredential.deserialize(vc) async def verify_presentation( - self, vp: VerifiableCredential, options: LDProofVCOptions + self, vp: VerifiablePresentation, options: LDProofVCOptions ) -> PresentationVerificationResult: """Verify a VP with a Linked Data Proof.""" if not options.challenge: diff --git a/aries_cloudagent/vc/vc_ld/models/presentation.py b/aries_cloudagent/vc/vc_ld/models/presentation.py new file mode 100644 index 0000000000..627001e451 --- /dev/null +++ b/aries_cloudagent/vc/vc_ld/models/presentation.py @@ -0,0 +1,63 @@ +"""Verifiable Presentation model.""" + +from typing import Optional, Sequence, Union + +from marshmallow import INCLUDE, fields +from ....messaging.models.base import BaseModel, BaseModelSchema +from ....messaging.valid import UUID4_EXAMPLE, UUID4_VALIDATE, StrOrDictField +from .linked_data_proof import LinkedDataProofSchema + + +class VerifiablePresentation(BaseModel): + """Single VerifiablePresentation object.""" + + class Meta: + """VerifiablePresentation metadata.""" + + schema_class = "VerifiablePresentationSchema" + unknown = INCLUDE + + def __init__( + self, + *, + id: Optional[str] = None, + contexts: Optional[Sequence[Union[str, dict]]] = None, + types: Optional[Sequence[str]] = None, + credentials: Optional[Sequence[dict]] = None, + proof: Optional[Sequence[dict]] = None, + ): + """Initialize VerifiablePresentation.""" + self.id = id + self.contexts = contexts + self.types = types + self.credentials = credentials + self.proof = proof + + +class VerifiablePresentationSchema(BaseModelSchema): + """Single Verifiable Presentation Schema.""" + + class Meta: + """VerifiablePresentationSchema metadata.""" + + model_class = VerifiablePresentation + unknown = INCLUDE + + id = fields.Str( + required=False, + validate=UUID4_VALIDATE, + metadata={"description": "ID", "example": UUID4_EXAMPLE}, + ) + contexts = fields.List(StrOrDictField(), data_key="@context") + types = fields.List( + fields.Str(required=False, metadata={"description": "Types"}), data_key="type" + ) + credentials = fields.List( + fields.Dict(required=False, metadata={"description": "Credentials"}), + data_key="verifiableCredential", + ) + proof = fields.Nested( + LinkedDataProofSchema(), + required=True, + metadata={"description": "The proof of the credential"}, + ) From 7eca31f0ce1d68cccacddd92542686a9aa64e9af Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 25 Oct 2023 14:44:57 -0400 Subject: [PATCH 06/16] fix: ld_proof handler tests Signed-off-by: Daniel Bluhm --- .../formats/ld_proof/models/cred_detail.py | 7 +- .../formats/ld_proof/tests/test_handler.py | 404 ++---------------- aries_cloudagent/vc/vc_ld/models/options.py | 2 +- 3 files changed, 50 insertions(+), 363 deletions(-) diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/models/cred_detail.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/models/cred_detail.py index 04212385c3..2e1f05f326 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/models/cred_detail.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/models/cred_detail.py @@ -4,10 +4,11 @@ from marshmallow import INCLUDE, fields + from .......messaging.models.base import BaseModel, BaseModelSchema from .......vc.vc_ld import CredentialSchema from .......vc.vc_ld.models.credential import VerifiableCredential -from .cred_detail_options import LDProofVCDetailOptions, LDProofVCDetailOptionsSchema +from .......vc.vc_ld.models.options import LDProofVCOptions, LDProofVCOptionsSchema class LDProofVCDetail(BaseModel): @@ -21,7 +22,7 @@ class Meta: def __init__( self, credential: Optional[Union[dict, VerifiableCredential]], - options: Optional[Union[dict, LDProofVCDetailOptions]], + options: Optional[Union[dict, LDProofVCOptions]], ) -> None: """Initialize the LDProofVCDetail instance.""" self.credential = credential @@ -70,7 +71,7 @@ class Meta: ) options = fields.Nested( - LDProofVCDetailOptionsSchema(), + LDProofVCOptionsSchema(), required=True, metadata={ "description": ( diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/tests/test_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/tests/test_handler.py index bc868d08ca..dc93826b12 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/tests/test_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/tests/test_handler.py @@ -6,60 +6,43 @@ from marshmallow import ValidationError from .. import handler as test_module - from .......core.in_memory import InMemoryProfile -from .......storage.vc_holder.base import VCHolder -from .......wallet.base import DIDInfo from .......messaging.decorators.attach_decorator import AttachDecorator -from .......did.did_key import DIDKey +from .......storage.vc_holder.base import VCHolder from .......storage.vc_holder.vc_record import VCRecord -from ..models.cred_detail import ( - LDProofVCDetail, -) -from .......vc.ld_proofs import ( - DocumentLoader, - DocumentVerificationResult, - CredentialIssuancePurpose, - AuthenticationProofPurpose, - Ed25519Signature2018, - Ed25519Signature2020, - BbsBlsSignature2020, -) +from .......vc.ld_proofs import DocumentLoader, DocumentVerificationResult from .......vc.ld_proofs.constants import ( SECURITY_CONTEXT_BBS_URL, SECURITY_CONTEXT_ED25519_2020_URL, ) +from .......vc.ld_proofs.error import LinkedDataProofException from .......vc.tests.document_loader import custom_document_loader +from .......vc.vc_ld.manager import VcLdpManager +from .......vc.vc_ld.models.credential import VerifiableCredential +from .......vc.vc_ld.models.options import LDProofVCOptions +from .......wallet.base import BaseWallet from .......wallet.default_verification_key_strategy import ( - DefaultVerificationKeyStrategy, BaseVerificationKeyStrategy, -) -from .......wallet.key_type import BLS12381G2, ED25519 -from .......wallet.error import WalletNotFoundError -from .......wallet.did_method import SOV -from .......wallet.base import BaseWallet - -from ....models.detail.ld_proof import V20CredExRecordLDProof -from ....models.cred_ex_record import V20CredExRecord -from ....messages.cred_proposal import V20CredProposal -from ....messages.cred_format import V20CredFormat -from ....messages.cred_issue import V20CredIssue -from ....messages.cred_offer import V20CredOffer -from ....messages.cred_request import ( - V20CredRequest, + DefaultVerificationKeyStrategy, ) from ....message_types import ( ATTACHMENT_FORMAT, - CRED_20_PROPOSAL, + CRED_20_ISSUE, CRED_20_OFFER, + CRED_20_PROPOSAL, CRED_20_REQUEST, - CRED_20_ISSUE, ) - +from ....messages.cred_format import V20CredFormat +from ....messages.cred_issue import V20CredIssue +from ....messages.cred_offer import V20CredOffer +from ....messages.cred_proposal import V20CredProposal +from ....messages.cred_request import V20CredRequest +from ....models.cred_ex_record import V20CredExRecord +from ....models.detail.ld_proof import V20CredExRecordLDProof from ...handler import V20CredFormatError - from ..handler import LDProofCredFormatHandler from ..handler import LOGGER as LD_PROOF_LOGGER +from ..models.cred_detail import LDProofVCDetail TEST_DID_SOV = "did:sov:LjgpST2rjsoxYegQDRm7EL" TEST_DID_KEY = "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" @@ -217,193 +200,6 @@ async def test_get_ld_proof_detail_record(self): assert await self.handler.get_detail_record(cred_ex_id) in details_ld_proof mock_warning.assert_called_once() - async def test_assert_can_issue_with_id_and_proof_type(self): - with self.assertRaises(V20CredFormatError) as context: - await self.handler._assert_can_issue_with_id_and_proof_type( - "issuer_id", "random_proof_type" - ) - assert ( - "Unable to sign credential with unsupported proof type random_proof_type" - in str(context.exception) - ) - - with self.assertRaises(V20CredFormatError) as context: - await self.handler._assert_can_issue_with_id_and_proof_type( - "not_did", Ed25519Signature2018.signature_type - ) - assert "Unable to issue credential with issuer id: not_did" in str( - context.exception - ) - - with mock.patch.object( - LDProofCredFormatHandler, - "_did_info_for_did", - mock.CoroutineMock(), - ) as mock_did_info: - did_info = DIDInfo( - did=TEST_DID_SOV, - verkey="verkey", - metadata={}, - method=SOV, - key_type=ED25519, - ) - mock_did_info.return_value = did_info - await self.handler._assert_can_issue_with_id_and_proof_type( - "did:key:found", Ed25519Signature2018.signature_type - ) - await self.handler._assert_can_issue_with_id_and_proof_type( - "did:key:found", Ed25519Signature2020.signature_type - ) - - invalid_did_info = DIDInfo( - did=TEST_DID_SOV, - verkey="verkey", - metadata={}, - method=SOV, - key_type=BLS12381G2, - ) - mock_did_info.return_value = invalid_did_info - with self.assertRaises(V20CredFormatError) as context: - await self.handler._assert_can_issue_with_id_and_proof_type( - "did:key:found", Ed25519Signature2018.signature_type - ) - assert "Unable to issue credential with issuer id" in str(context.exception) - - mock_did_info.side_effect = (WalletNotFoundError,) - with self.assertRaises(V20CredFormatError) as context: - await self.handler._assert_can_issue_with_id_and_proof_type( - "did:key:notfound", Ed25519Signature2018.signature_type - ) - assert "Issuer did did:key:notfound not found" in str(context.exception) - - async def test_get_did_info_for_did_sov(self): - self.wallet.get_local_did = mock.CoroutineMock() - - did_info = await self.handler._did_info_for_did(TEST_DID_SOV) - self.wallet.get_local_did.assert_called_once_with( - TEST_DID_SOV.replace("did:sov:", "") - ) - assert did_info == self.wallet.get_local_did.return_value - - async def test_get_did_info_for_did_key(self): - self.wallet.get_local_did.reset_mock() - - did_info = await self.handler._did_info_for_did(TEST_DID_KEY) - self.wallet.get_local_did.assert_called_once_with(TEST_DID_KEY) - assert did_info == self.wallet.get_local_did.return_value - - async def test_get_suite_for_detail(self): - detail: LDProofVCDetail = LDProofVCDetail.deserialize(LD_PROOF_VC_DETAIL) - - with mock.patch.object( - LDProofCredFormatHandler, - "_assert_can_issue_with_id_and_proof_type", - mock.CoroutineMock(), - ) as mock_can_issue, mock.patch.object( - LDProofCredFormatHandler, - "_did_info_for_did", - mock.CoroutineMock(), - ) as mock_did_info: - suite = await self.handler._get_suite_for_detail(detail) - - assert suite.signature_type == detail.options.proof_type - assert type(suite) == Ed25519Signature2018 - assert suite.verification_method == DIDKey.from_did(TEST_DID_KEY).key_id - assert suite.proof == {"created": LD_PROOF_VC_DETAIL["options"]["created"]} - assert suite.key_pair.key_type == ED25519 - assert suite.key_pair.public_key_base58 == mock_did_info.return_value.verkey - - mock_can_issue.assert_called_once_with( - detail.credential.issuer_id, detail.options.proof_type - ) - mock_did_info.assert_called_once_with(detail.credential.issuer_id) - - async def test_get_suite(self): - proof = mock.MagicMock() - did_info = mock.MagicMock() - - suite = await self.handler._get_suite( - proof_type=BbsBlsSignature2020.signature_type, - verification_method="verification_method", - proof=proof, - did_info=did_info, - ) - - assert type(suite) == BbsBlsSignature2020 - assert suite.verification_method == "verification_method" - assert suite.proof == proof - assert suite.key_pair.key_type == BLS12381G2 - assert suite.key_pair.public_key_base58 == did_info.verkey - - suite = await self.handler._get_suite( - proof_type=Ed25519Signature2018.signature_type, - verification_method="verification_method", - proof=proof, - did_info=did_info, - ) - - assert type(suite) == Ed25519Signature2018 - assert suite.verification_method == "verification_method" - assert suite.proof == proof - assert suite.key_pair.key_type == ED25519 - assert suite.key_pair.public_key_base58 == did_info.verkey - - suite = await self.handler._get_suite( - proof_type=Ed25519Signature2020.signature_type, - verification_method="verification_method", - proof=proof, - did_info=did_info, - ) - - assert type(suite) == Ed25519Signature2020 - assert suite.verification_method == "verification_method" - assert suite.proof == proof - assert suite.key_pair.key_type == ED25519 - assert suite.key_pair.public_key_base58 == did_info.verkey - - async def test_get_proof_purpose(self): - purpose = self.handler._get_proof_purpose() - assert type(purpose) == CredentialIssuancePurpose - - purpose: AuthenticationProofPurpose = self.handler._get_proof_purpose( - proof_purpose=AuthenticationProofPurpose.term, - challenge="challenge", - domain="domain", - ) - assert type(purpose) == AuthenticationProofPurpose - assert purpose.domain == "domain" - assert purpose.challenge == "challenge" - - with self.assertRaises(V20CredFormatError) as context: - self.handler._get_proof_purpose( - proof_purpose=AuthenticationProofPurpose.term - ) - assert "Challenge is required for" in str(context.exception) - - with self.assertRaises(V20CredFormatError) as context: - self.handler._get_proof_purpose(proof_purpose="random") - assert "Unsupported proof purpose: random" in str(context.exception) - - async def test_prepare_detail(self): - detail: LDProofVCDetail = LDProofVCDetail.deserialize(LD_PROOF_VC_DETAIL) - detail.options.proof_type = BbsBlsSignature2020.signature_type - - assert SECURITY_CONTEXT_BBS_URL not in detail.credential.context_urls - - detail = await self.handler._prepare_detail(detail) - - assert SECURITY_CONTEXT_BBS_URL in detail.credential.context_urls - - async def test_prepare_detail_ed25519_2020(self): - detail: LDProofVCDetail = LDProofVCDetail.deserialize(LD_PROOF_VC_DETAIL) - detail.options.proof_type = Ed25519Signature2020.signature_type - - assert SECURITY_CONTEXT_ED25519_2020_URL not in detail.credential.context_urls - - detail = await self.handler._prepare_detail(detail) - - assert SECURITY_CONTEXT_ED25519_2020_URL in detail.credential.context_urls - async def test_create_proposal(self): cred_ex_record = mock.MagicMock() @@ -452,8 +248,8 @@ async def test_receive_proposal(self): async def test_create_offer(self): with mock.patch.object( - LDProofCredFormatHandler, - "_assert_can_issue_with_id_and_proof_type", + VcLdpManager, + "assert_can_issue_with_id_and_proof_type", mock.CoroutineMock(), ) as mock_can_issue, patch.object( test_module, "get_properties_without_context", return_value=[] @@ -492,8 +288,8 @@ async def test_create_offer_adds_bbs_context(self): ) with mock.patch.object( - LDProofCredFormatHandler, - "_assert_can_issue_with_id_and_proof_type", + VcLdpManager, + "assert_can_issue_with_id_and_proof_type", mock.CoroutineMock(), ), patch.object(test_module, "get_properties_without_context", return_value=[]): (cred_format, attachment) = await self.handler.create_offer(cred_proposal) @@ -517,8 +313,8 @@ async def test_create_offer_adds_ed25519_2020_context(self): ) with mock.patch.object( - LDProofCredFormatHandler, - "_assert_can_issue_with_id_and_proof_type", + VcLdpManager, + "assert_can_issue_with_id_and_proof_type", mock.CoroutineMock(), ), patch.object(test_module, "get_properties_without_context", return_value=[]): (cred_format, attachment) = await self.handler.create_offer(cred_proposal) @@ -539,8 +335,8 @@ async def test_create_offer_x_no_proposal(self): async def test_create_offer_x_wrong_attributes(self): missing_properties = ["foo"] with mock.patch.object( - LDProofCredFormatHandler, - "_assert_can_issue_with_id_and_proof_type", + VcLdpManager, + "assert_can_issue_with_id_and_proof_type", mock.CoroutineMock(), ), patch.object( test_module, @@ -788,27 +584,21 @@ async def test_issue_credential(self): ) with mock.patch.object( - LDProofCredFormatHandler, - "_get_suite_for_detail", - mock.CoroutineMock(), - ) as mock_get_suite, mock.patch.object( - test_module, "issue", mock.CoroutineMock(return_value=LD_PROOF_VC) - ) as mock_issue, mock.patch.object( - LDProofCredFormatHandler, - "_get_proof_purpose", - ) as mock_get_proof_purpose: + VcLdpManager, + "issue", + mock.CoroutineMock( + return_value=VerifiableCredential.deserialize(LD_PROOF_VC) + ), + ) as mock_issue: (cred_format, attachment) = await self.handler.issue_credential( cred_ex_record ) detail = LDProofVCDetail.deserialize(LD_PROOF_VC_DETAIL) - mock_get_suite.assert_called_once_with(detail, None) mock_issue.assert_called_once_with( - credential=LD_PROOF_VC_DETAIL["credential"], - suite=mock_get_suite.return_value, - document_loader=custom_document_loader, - purpose=mock_get_proof_purpose.return_value, + VerifiableCredential.deserialize(LD_PROOF_VC_DETAIL["credential"]), + LDProofVCOptions.deserialize(LD_PROOF_VC_DETAIL["options"]), ) # assert identifier match @@ -820,98 +610,6 @@ async def test_issue_credential(self): # assert data is encoded as base64 assert attachment.data.base64 - async def test_issue_credential_adds_bbs_context(self): - cred_request = V20CredRequest( - formats=[ - V20CredFormat( - attach_id="0", - format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ - V20CredFormat.Format.LD_PROOF.api - ], - ) - ], - requests_attach=[ - AttachDecorator.data_base64(LD_PROOF_VC_DETAIL_BBS, ident="0") - ], - ) - - cred_ex_record = V20CredExRecord( - cred_ex_id="dummy-cxid", - cred_request=cred_request, - ) - - with mock.patch.object( - LDProofCredFormatHandler, - "_get_suite_for_detail", - mock.CoroutineMock(), - ) as mock_get_suite, mock.patch.object( - test_module, "issue", mock.CoroutineMock(return_value=LD_PROOF_VC) - ) as mock_issue, mock.patch.object( - LDProofCredFormatHandler, - "_get_proof_purpose", - ) as mock_get_proof_purpose: - (cred_format, attachment) = await self.handler.issue_credential( - cred_ex_record - ) - - credential_with_bbs = deepcopy(LD_PROOF_VC_DETAIL_BBS["credential"]) - credential_with_bbs["@context"].append(SECURITY_CONTEXT_BBS_URL) - - mock_issue.assert_called_once_with( - credential=credential_with_bbs, - suite=mock_get_suite.return_value, - document_loader=custom_document_loader, - purpose=mock_get_proof_purpose.return_value, - ) - - async def test_issue_credential_adds_ed25519_2020_context(self): - cred_request = V20CredRequest( - formats=[ - V20CredFormat( - attach_id="0", - format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ - V20CredFormat.Format.LD_PROOF.api - ], - ) - ], - requests_attach=[ - AttachDecorator.data_base64(LD_PROOF_VC_DETAIL_ED25519_2020, ident="0") - ], - ) - - cred_ex_record = V20CredExRecord( - cred_ex_id="dummy-cxid", - cred_request=cred_request, - ) - - with mock.patch.object( - LDProofCredFormatHandler, - "_get_suite_for_detail", - mock.CoroutineMock(), - ) as mock_get_suite, mock.patch.object( - test_module, "issue", mock.CoroutineMock(return_value=LD_PROOF_VC) - ) as mock_issue, mock.patch.object( - LDProofCredFormatHandler, - "_get_proof_purpose", - ) as mock_get_proof_purpose: - (cred_format, attachment) = await self.handler.issue_credential( - cred_ex_record - ) - - credential_with_ed25519_2020 = deepcopy( - LD_PROOF_VC_DETAIL_ED25519_2020["credential"] - ) - credential_with_ed25519_2020["@context"].append( - SECURITY_CONTEXT_ED25519_2020_URL - ) - - mock_issue.assert_called_once_with( - credential=credential_with_ed25519_2020, - suite=mock_get_suite.return_value, - document_loader=custom_document_loader, - purpose=mock_get_proof_purpose.return_value, - ) - async def test_issue_credential_x_no_data(self): cred_ex_record = V20CredExRecord() @@ -1144,28 +842,14 @@ async def test_store_credential(self): self.holder.store_credential = mock.CoroutineMock() with mock.patch.object( - LDProofCredFormatHandler, - "_get_suite", - mock.CoroutineMock(), - ) as mock_get_suite, mock.patch.object( - test_module, + VcLdpManager, "verify_credential", - mock.CoroutineMock(return_value=DocumentVerificationResult(verified=True)), - ) as mock_verify_credential, mock.patch.object( - LDProofCredFormatHandler, - "_get_proof_purpose", - ) as mock_get_proof_purpose: + mock.CoroutineMock( + return_value=DocumentVerificationResult(verified=True) + ), + ) as mock_verify_credential: await self.handler.store_credential(cred_ex_record, cred_id) - mock_get_suite.assert_called_once_with( - proof_type=LD_PROOF_VC["proof"]["type"] - ) - mock_verify_credential.assert_called_once_with( - credential=LD_PROOF_VC, - suites=[mock_get_suite.return_value], - document_loader=custom_document_loader, - purpose=mock_get_proof_purpose.return_value, - ) self.holder.store_credential.assert_called_once_with( VCRecord( contexts=LD_PROOF_VC["@context"], @@ -1205,15 +889,17 @@ async def test_store_credential_x_not_verified(self): self.holder.store_credential = mock.CoroutineMock() with mock.patch.object( - LDProofCredFormatHandler, + VcLdpManager, "_get_suite", mock.CoroutineMock(), ) as mock_get_suite, mock.patch.object( - test_module, + VcLdpManager, "verify_credential", - mock.CoroutineMock(return_value=DocumentVerificationResult(verified=False)), + mock.CoroutineMock( + return_value=DocumentVerificationResult(verified=False) + ), ) as mock_verify_credential, mock.patch.object( - LDProofCredFormatHandler, + VcLdpManager, "_get_proof_purpose", ) as mock_get_proof_purpose, self.assertRaises( V20CredFormatError diff --git a/aries_cloudagent/vc/vc_ld/models/options.py b/aries_cloudagent/vc/vc_ld/models/options.py index 412b43c5a3..949759b3c6 100644 --- a/aries_cloudagent/vc/vc_ld/models/options.py +++ b/aries_cloudagent/vc/vc_ld/models/options.py @@ -89,7 +89,7 @@ class Meta: verification_method = fields.Str( data_key="verificationMethod", - required=True, + required=False, metadata={ "description": ( "The verification method to use for the proof. Should match a" From 9de833db3faf61d7ce6dcd11de8e32d5d20924e9 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 25 Oct 2023 14:48:27 -0400 Subject: [PATCH 07/16] test: vc ldp manager Signed-off-by: Daniel Bluhm --- .../vc/vc_ld/tests/test_manager.py | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 aries_cloudagent/vc/vc_ld/tests/test_manager.py diff --git a/aries_cloudagent/vc/vc_ld/tests/test_manager.py b/aries_cloudagent/vc/vc_ld/tests/test_manager.py new file mode 100644 index 0000000000..e1cf05171d --- /dev/null +++ b/aries_cloudagent/vc/vc_ld/tests/test_manager.py @@ -0,0 +1,283 @@ +"""Test VcLdpManager.""" +from asynctest import MagicMock, mock as async_mock +import pytest + +from ....core.in_memory.profile import InMemoryProfile +from ....core.profile import Profile +from ....did.did_key import DIDKey +from ....wallet.base import BaseWallet +from ....wallet.did_info import DIDInfo +from ....wallet.did_method import SOV +from ....wallet.error import WalletNotFoundError +from ....wallet.key_type import BLS12381G2, ED25519 +from ...ld_proofs.constants import ( + SECURITY_CONTEXT_BBS_URL, + SECURITY_CONTEXT_ED25519_2020_URL, +) +from ...ld_proofs.crypto.wallet_key_pair import WalletKeyPair +from ...ld_proofs.purposes.authentication_proof_purpose import ( + AuthenticationProofPurpose, +) +from ...ld_proofs.purposes.credential_issuance_purpose import CredentialIssuancePurpose +from ...ld_proofs.suites.bbs_bls_signature_2020 import BbsBlsSignature2020 +from ...ld_proofs.suites.ed25519_signature_2018 import Ed25519Signature2018 +from ...ld_proofs.suites.ed25519_signature_2020 import Ed25519Signature2020 +from ..manager import VcLdpManager, VcLdpManagerError +from ..models.credential import VerifiableCredential +from ..models.options import LDProofVCOptions + +TEST_DID_SOV = "did:sov:LjgpST2rjsoxYegQDRm7EL" +TEST_DID_KEY = "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" +VC = { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "credentialSubject": {"test": "key"}, + "issuanceDate": "2021-04-12", + "issuer": TEST_DID_KEY, + }, + "options": { + "proofType": "Ed25519Signature2018", + "created": "2019-12-11T03:50:55", + }, +} + + +@pytest.fixture +def wallet(): + yield MagicMock(BaseWallet, autospec=True) + + +@pytest.fixture +def profile(): + profile = InMemoryProfile.test_profile() + yield profile + + +@pytest.fixture +def manager(profile: Profile): + yield VcLdpManager(profile) + + +@pytest.fixture +def vc(): + yield VerifiableCredential.deserialize(VC["credential"]) + + +@pytest.fixture +def options(): + yield LDProofVCOptions.deserialize(VC["options"]) + + +async def test_assert_can_issue_with_id_and_proof_type(manager: VcLdpManager): + with pytest.raises(VcLdpManagerError) as context: + await manager.assert_can_issue_with_id_and_proof_type( + "issuer_id", "random_proof_type" + ) + + assert ( + "Unable to sign credential with unsupported proof type random_proof_type" + in str(context.value) + ) + + with pytest.raises(VcLdpManagerError) as context: + await manager.assert_can_issue_with_id_and_proof_type( + "not_did", Ed25519Signature2018.signature_type + ) + assert "Unable to issue credential with issuer id: not_did" in str( + context.value + ) + + with async_mock.patch.object( + manager, + "_did_info_for_did", + async_mock.CoroutineMock(), + ) as mock_did_info: + did_info = DIDInfo( + did=TEST_DID_SOV, + verkey="verkey", + metadata={}, + method=SOV, + key_type=ED25519, + ) + mock_did_info.return_value = did_info + await manager.assert_can_issue_with_id_and_proof_type( + "did:key:found", Ed25519Signature2018.signature_type + ) + await manager.assert_can_issue_with_id_and_proof_type( + "did:key:found", Ed25519Signature2020.signature_type + ) + + invalid_did_info = DIDInfo( + did=TEST_DID_SOV, + verkey="verkey", + metadata={}, + method=SOV, + key_type=BLS12381G2, + ) + mock_did_info.return_value = invalid_did_info + with pytest.raises(VcLdpManagerError) as context: + await manager.assert_can_issue_with_id_and_proof_type( + "did:key:found", Ed25519Signature2018.signature_type + ) + assert "Unable to issue credential with issuer id" in str(context.value) + + mock_did_info.side_effect = (WalletNotFoundError,) + with pytest.raises(VcLdpManagerError) as context: + await manager.assert_can_issue_with_id_and_proof_type( + "did:key:notfound", Ed25519Signature2018.signature_type + ) + assert "Issuer did did:key:notfound not found" in str(context.value) + + +async def test_get_did_info_for_did_sov(manager: VcLdpManager, wallet: MagicMock): + wallet.get_local_did = async_mock.CoroutineMock() + + did_info = await manager._did_info_for_did(TEST_DID_SOV) + wallet.get_local_did.assert_called_once_with(TEST_DID_SOV.replace("did:sov:", "")) + assert did_info == wallet.get_local_did.return_value + + +async def test_get_did_info_for_did_key(manager: VcLdpManager, wallet: MagicMock): + wallet.get_local_did.reset_mock() + + did_info = await manager._did_info_for_did(TEST_DID_KEY) + wallet.get_local_did.assert_called_once_with(TEST_DID_KEY) + assert did_info == wallet.get_local_did.return_value + + +async def test_get_suite_for_credential(manager: VcLdpManager): + vc = VerifiableCredential.deserialize(VC["credential"]) + options = LDProofVCOptions.deserialize(VC["options"]) + + with async_mock.patch.object( + manager, + "_assert_can_issue_with_id_and_proof_type", + async_mock.CoroutineMock(), + ) as mock_can_issue, async_mock.patch.object( + manager, + "_did_info_for_did", + async_mock.CoroutineMock(), + ) as mock_did_info: + suite = await manager._get_suite_for_credential(vc, options) + + assert suite.signature_type == options.proof_type + assert isinstance(suite, Ed25519Signature2018) + assert suite.verification_method == DIDKey.from_did(TEST_DID_KEY).key_id + assert suite.proof == {"created": VC["options"]["created"]} + assert isinstance(suite.key_pair, WalletKeyPair) + assert suite.key_pair.key_type == ED25519 + assert suite.key_pair.public_key_base58 == mock_did_info.return_value.verkey + + mock_can_issue.assert_called_once_with(vc.issuer_id, options.proof_type) + mock_did_info.assert_called_once_with(vc.issuer_id) + + +async def test_get_suite(manager: VcLdpManager): + proof = async_mock.MagicMock() + did_info = async_mock.MagicMock() + + suite = await manager._get_suite( + proof_type=BbsBlsSignature2020.signature_type, + verification_method="verification_method", + proof=proof, + did_info=did_info, + ) + + assert isinstance(suite, BbsBlsSignature2020) + assert suite.verification_method == "verification_method" + assert suite.proof == proof + assert isinstance(suite.key_pair, WalletKeyPair) + assert suite.key_pair.key_type == BLS12381G2 + assert suite.key_pair.public_key_base58 == did_info.verkey + + suite = await manager._get_suite( + proof_type=Ed25519Signature2018.signature_type, + verification_method="verification_method", + proof=proof, + did_info=did_info, + ) + + assert isinstance(suite, Ed25519Signature2018) + assert suite.verification_method == "verification_method" + assert suite.proof == proof + assert isinstance(suite.key_pair, WalletKeyPair) + assert suite.key_pair.key_type == ED25519 + assert suite.key_pair.public_key_base58 == did_info.verkey + + suite = await manager._get_suite( + proof_type=Ed25519Signature2020.signature_type, + verification_method="verification_method", + proof=proof, + did_info=did_info, + ) + + assert isinstance(suite, Ed25519Signature2020) + assert suite.verification_method == "verification_method" + assert suite.proof == proof + assert isinstance(suite.key_pair, WalletKeyPair) + assert suite.key_pair.key_type == ED25519 + assert suite.key_pair.public_key_base58 == did_info.verkey + + +async def test_get_proof_purpose(manager: VcLdpManager): + purpose = manager._get_proof_purpose() + assert isinstance(purpose, CredentialIssuancePurpose) + + purpose = manager._get_proof_purpose( + proof_purpose=AuthenticationProofPurpose.term, + challenge="challenge", + domain="domain", + ) + assert isinstance(purpose, AuthenticationProofPurpose) + assert purpose.domain == "domain" + assert purpose.challenge == "challenge" + + with pytest.raises(VcLdpManagerError) as context: + manager._get_proof_purpose(proof_purpose=AuthenticationProofPurpose.term) + assert "Challenge is required for" in str(context.value) + + with pytest.raises(VcLdpManagerError) as context: + manager._get_proof_purpose(proof_purpose="random") + assert "Unsupported proof purpose: random" in str(context.value) + + +async def test_prepare_detail( + manager: VcLdpManager, vc: VerifiableCredential, options: LDProofVCOptions +): + options.proof_type = BbsBlsSignature2020.signature_type + + assert SECURITY_CONTEXT_BBS_URL not in vc.context_urls + + detail = await manager.prepare_credential(vc, options) + + assert SECURITY_CONTEXT_BBS_URL in vc.context_urls + + +async def test_prepare_detail_ed25519_2020( + manager: VcLdpManager, vc: VerifiableCredential, options: LDProofVCOptions +): + options.proof_type = Ed25519Signature2020.signature_type + + assert SECURITY_CONTEXT_ED25519_2020_URL not in vc.context_urls + + detail = await manager.prepare_credential(vc, options) + + assert SECURITY_CONTEXT_ED25519_2020_URL in vc.context_urls + + +async def test_issue(): + raise NotImplementedError() + + +async def test_issue_ed25519_2020(): + """Ensure ed25519 2020 context added to issued cred.""" + raise NotImplementedError() + + +async def test_issue_bbs(): + """Ensure BBS context is added to issued cred.""" + raise NotImplementedError() From ee9ef3cdb482616c3f5168d5c0b65d8cab54494d Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 25 Oct 2023 15:07:08 -0400 Subject: [PATCH 08/16] fix: dif format handler tests Signed-off-by: Daniel Bluhm --- .../v2_0/formats/dif/tests/test_handler.py | 46 +++++-------------- aries_cloudagent/vc/vc_ld/models/options.py | 2 +- .../vc/vc_ld/models/presentation.py | 2 + .../vc/vc_ld/tests/test_manager.py | 14 ++++++ 4 files changed, 29 insertions(+), 35 deletions(-) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py index 3020607be9..ddee1de370 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py @@ -4,46 +4,36 @@ from marshmallow import ValidationError from pyld import jsonld -from aries_cloudagent.protocols.present_proof.dif.pres_exch import SchemaInputDescriptor - +from .. import handler as test_module from .......core.in_memory import InMemoryProfile from .......messaging.decorators.attach_decorator import AttachDecorator -from .......messaging.responder import MockResponder, BaseResponder +from .......messaging.responder import BaseResponder, MockResponder from .......storage.vc_holder.base import VCHolder from .......storage.vc_holder.vc_record import VCRecord -from .......vc.ld_proofs import ( - DocumentLoader, - Ed25519Signature2018, - Ed25519Signature2020, - BbsBlsSignature2020, - BbsBlsSignatureProof2020, -) +from .......vc.ld_proofs import DocumentLoader from .......vc.tests.document_loader import custom_document_loader +from .......vc.vc_ld.manager import VcLdpManager from .......vc.vc_ld.validation_result import PresentationVerificationResult from .......wallet.base import BaseWallet - -from .....dif.pres_exch_handler import DIFPresExchHandler, DIFPresExchError +from .....dif.pres_exch import SchemaInputDescriptor +from .....dif.pres_exch_handler import DIFPresExchError, DIFPresExchHandler from .....dif.tests.test_data import ( - TEST_CRED_DICT, EXPANDED_CRED_FHIR_TYPE_1, EXPANDED_CRED_FHIR_TYPE_2, + TEST_CRED_DICT, ) - from ....message_types import ( ATTACHMENT_FORMAT, - PRES_20_REQUEST, PRES_20, PRES_20_PROPOSAL, + PRES_20_REQUEST, ) from ....messages.pres import V20Pres +from ....messages.pres_format import V20PresFormat from ....messages.pres_proposal import V20PresProposal from ....messages.pres_request import V20PresRequest -from ....messages.pres_format import V20PresFormat from ....models.pres_exchange import V20PresExRecord - from ...handler import V20PresFormatHandlerError - -from .. import handler as test_module from ..handler import DIFPresFormatHandler TEST_DID_SOV = "did:sov:LjgpST2rjsoxYegQDRm7EL" @@ -393,18 +383,6 @@ def test_validate_fields(self): incorrect_pres.pop("@context") self.handler.validate_fields(PRES_20, incorrect_pres) - async def test_get_all_suites(self): - suites = await self.handler._get_all_suites() - assert len(suites) == 4 - types = [ - Ed25519Signature2018, - Ed25519Signature2020, - BbsBlsSignature2020, - BbsBlsSignatureProof2020, - ] - for suite in suites: - assert type(suite) in types - async def test_create_bound_request_a(self): dif_proposal_dict = { "input_descriptors": [ @@ -1162,7 +1140,7 @@ async def test_verify_pres_sequence(self): ) with mock.patch.object( - test_module, + VcLdpManager, "verify_presentation", mock.CoroutineMock( return_value=PresentationVerificationResult(verified=True) @@ -1172,7 +1150,7 @@ async def test_verify_pres_sequence(self): assert output.verified with mock.patch.object( - test_module, + VcLdpManager, "verify_presentation", mock.CoroutineMock( return_value=PresentationVerificationResult(verified=False) @@ -1219,7 +1197,7 @@ async def test_verify_pres(self): ) with mock.patch.object( - test_module, + VcLdpManager, "verify_presentation", mock.CoroutineMock( return_value=PresentationVerificationResult(verified=True) diff --git a/aries_cloudagent/vc/vc_ld/models/options.py b/aries_cloudagent/vc/vc_ld/models/options.py index 949759b3c6..f34ef3c5ab 100644 --- a/aries_cloudagent/vc/vc_ld/models/options.py +++ b/aries_cloudagent/vc/vc_ld/models/options.py @@ -101,7 +101,7 @@ class Meta: proof_type = fields.Str( data_key="proofType", - required=True, + required=False, metadata={ "description": ( "The proof type used for the proof. Should match suites registered in" diff --git a/aries_cloudagent/vc/vc_ld/models/presentation.py b/aries_cloudagent/vc/vc_ld/models/presentation.py index 627001e451..0e467c9fa5 100644 --- a/aries_cloudagent/vc/vc_ld/models/presentation.py +++ b/aries_cloudagent/vc/vc_ld/models/presentation.py @@ -25,8 +25,10 @@ def __init__( types: Optional[Sequence[str]] = None, credentials: Optional[Sequence[dict]] = None, proof: Optional[Sequence[dict]] = None, + **kwargs, ): """Initialize VerifiablePresentation.""" + super().__init__() self.id = id self.contexts = contexts self.types = types diff --git a/aries_cloudagent/vc/vc_ld/tests/test_manager.py b/aries_cloudagent/vc/vc_ld/tests/test_manager.py index e1cf05171d..ebe8957e84 100644 --- a/aries_cloudagent/vc/vc_ld/tests/test_manager.py +++ b/aries_cloudagent/vc/vc_ld/tests/test_manager.py @@ -20,6 +20,7 @@ ) from ...ld_proofs.purposes.credential_issuance_purpose import CredentialIssuancePurpose from ...ld_proofs.suites.bbs_bls_signature_2020 import BbsBlsSignature2020 +from ...ld_proofs.suites.bbs_bls_signature_proof_2020 import BbsBlsSignatureProof2020 from ...ld_proofs.suites.ed25519_signature_2018 import Ed25519Signature2018 from ...ld_proofs.suites.ed25519_signature_2020 import Ed25519Signature2020 from ..manager import VcLdpManager, VcLdpManagerError @@ -281,3 +282,16 @@ async def test_issue_ed25519_2020(): async def test_issue_bbs(): """Ensure BBS context is added to issued cred.""" raise NotImplementedError() + + +async def test_get_all_suites(manager: VcLdpManager): + suites = await manager._get_all_suites() + assert len(suites) == 4 + types = ( + Ed25519Signature2018, + Ed25519Signature2020, + BbsBlsSignature2020, + BbsBlsSignatureProof2020, + ) + for suite in suites: + assert isinstance(suite, types) From fe98313d2c30ef70d9e90fdecf4cfbb2aedb4e84 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 25 Oct 2023 15:11:25 -0400 Subject: [PATCH 09/16] fix: missing decorators on vc ldp manager tests Signed-off-by: Daniel Bluhm --- aries_cloudagent/vc/vc_ld/tests/test_manager.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/aries_cloudagent/vc/vc_ld/tests/test_manager.py b/aries_cloudagent/vc/vc_ld/tests/test_manager.py index ebe8957e84..3c8810281c 100644 --- a/aries_cloudagent/vc/vc_ld/tests/test_manager.py +++ b/aries_cloudagent/vc/vc_ld/tests/test_manager.py @@ -73,6 +73,7 @@ def options(): yield LDProofVCOptions.deserialize(VC["options"]) +@pytest.mark.asyncio async def test_assert_can_issue_with_id_and_proof_type(manager: VcLdpManager): with pytest.raises(VcLdpManagerError) as context: await manager.assert_can_issue_with_id_and_proof_type( @@ -134,6 +135,7 @@ async def test_assert_can_issue_with_id_and_proof_type(manager: VcLdpManager): assert "Issuer did did:key:notfound not found" in str(context.value) +@pytest.mark.asyncio async def test_get_did_info_for_did_sov(manager: VcLdpManager, wallet: MagicMock): wallet.get_local_did = async_mock.CoroutineMock() @@ -142,6 +144,7 @@ async def test_get_did_info_for_did_sov(manager: VcLdpManager, wallet: MagicMock assert did_info == wallet.get_local_did.return_value +@pytest.mark.asyncio async def test_get_did_info_for_did_key(manager: VcLdpManager, wallet: MagicMock): wallet.get_local_did.reset_mock() @@ -150,6 +153,7 @@ async def test_get_did_info_for_did_key(manager: VcLdpManager, wallet: MagicMock assert did_info == wallet.get_local_did.return_value +@pytest.mark.asyncio async def test_get_suite_for_credential(manager: VcLdpManager): vc = VerifiableCredential.deserialize(VC["credential"]) options = LDProofVCOptions.deserialize(VC["options"]) @@ -177,6 +181,7 @@ async def test_get_suite_for_credential(manager: VcLdpManager): mock_did_info.assert_called_once_with(vc.issuer_id) +@pytest.mark.asyncio async def test_get_suite(manager: VcLdpManager): proof = async_mock.MagicMock() did_info = async_mock.MagicMock() @@ -224,6 +229,7 @@ async def test_get_suite(manager: VcLdpManager): assert suite.key_pair.public_key_base58 == did_info.verkey +@pytest.mark.asyncio async def test_get_proof_purpose(manager: VcLdpManager): purpose = manager._get_proof_purpose() assert isinstance(purpose, CredentialIssuancePurpose) @@ -246,6 +252,7 @@ async def test_get_proof_purpose(manager: VcLdpManager): assert "Unsupported proof purpose: random" in str(context.value) +@pytest.mark.asyncio async def test_prepare_detail( manager: VcLdpManager, vc: VerifiableCredential, options: LDProofVCOptions ): @@ -258,6 +265,7 @@ async def test_prepare_detail( assert SECURITY_CONTEXT_BBS_URL in vc.context_urls +@pytest.mark.asyncio async def test_prepare_detail_ed25519_2020( manager: VcLdpManager, vc: VerifiableCredential, options: LDProofVCOptions ): @@ -270,20 +278,24 @@ async def test_prepare_detail_ed25519_2020( assert SECURITY_CONTEXT_ED25519_2020_URL in vc.context_urls +@pytest.mark.asyncio async def test_issue(): raise NotImplementedError() +@pytest.mark.asyncio async def test_issue_ed25519_2020(): """Ensure ed25519 2020 context added to issued cred.""" raise NotImplementedError() +@pytest.mark.asyncio async def test_issue_bbs(): """Ensure BBS context is added to issued cred.""" raise NotImplementedError() +@pytest.mark.asyncio async def test_get_all_suites(manager: VcLdpManager): suites = await manager._get_all_suites() assert len(suites) == 4 From ccb2454f863595adee6709f6129354fa00c2aa4d Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 17 Nov 2023 13:33:50 -0500 Subject: [PATCH 10/16] style: formatting fixes after rebase Signed-off-by: Daniel Bluhm --- .../v2_0/formats/ld_proof/tests/test_handler.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/tests/test_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/tests/test_handler.py index dc93826b12..2d13ebc23c 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/tests/test_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/tests/test_handler.py @@ -15,7 +15,6 @@ SECURITY_CONTEXT_BBS_URL, SECURITY_CONTEXT_ED25519_2020_URL, ) -from .......vc.ld_proofs.error import LinkedDataProofException from .......vc.tests.document_loader import custom_document_loader from .......vc.vc_ld.manager import VcLdpManager from .......vc.vc_ld.models.credential import VerifiableCredential @@ -844,9 +843,7 @@ async def test_store_credential(self): with mock.patch.object( VcLdpManager, "verify_credential", - mock.CoroutineMock( - return_value=DocumentVerificationResult(verified=True) - ), + mock.CoroutineMock(return_value=DocumentVerificationResult(verified=True)), ) as mock_verify_credential: await self.handler.store_credential(cred_ex_record, cred_id) @@ -895,9 +892,7 @@ async def test_store_credential_x_not_verified(self): ) as mock_get_suite, mock.patch.object( VcLdpManager, "verify_credential", - mock.CoroutineMock( - return_value=DocumentVerificationResult(verified=False) - ), + mock.CoroutineMock(return_value=DocumentVerificationResult(verified=False)), ) as mock_verify_credential, mock.patch.object( VcLdpManager, "_get_proof_purpose", From 952f3cf007891a9cc1ac4b6bc9c5eafc69311234 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Sat, 18 Nov 2023 16:15:17 -0500 Subject: [PATCH 11/16] fix: tests for vc ldp manager Signed-off-by: Daniel Bluhm --- .../vc/vc_ld/tests/test_manager.py | 138 +++++++++++++----- 1 file changed, 99 insertions(+), 39 deletions(-) diff --git a/aries_cloudagent/vc/vc_ld/tests/test_manager.py b/aries_cloudagent/vc/vc_ld/tests/test_manager.py index 3c8810281c..8264bf456b 100644 --- a/aries_cloudagent/vc/vc_ld/tests/test_manager.py +++ b/aries_cloudagent/vc/vc_ld/tests/test_manager.py @@ -1,13 +1,21 @@ """Test VcLdpManager.""" -from asynctest import MagicMock, mock as async_mock import pytest +from aries_cloudagent.resolver.default.key import KeyDIDResolver +from aries_cloudagent.resolver.did_resolver import DIDResolver + +from aries_cloudagent.tests import mock +from aries_cloudagent.vc.ld_proofs.document_loader import DocumentLoader from ....core.in_memory.profile import InMemoryProfile from ....core.profile import Profile from ....did.did_key import DIDKey from ....wallet.base import BaseWallet +from ....wallet.default_verification_key_strategy import ( + BaseVerificationKeyStrategy, + DefaultVerificationKeyStrategy, +) from ....wallet.did_info import DIDInfo -from ....wallet.did_method import SOV +from ....wallet.did_method import DIDMethod, DIDMethods, KEY, SOV from ....wallet.error import WalletNotFoundError from ....wallet.key_type import BLS12381G2, ED25519 from ...ld_proofs.constants import ( @@ -20,7 +28,6 @@ ) from ...ld_proofs.purposes.credential_issuance_purpose import CredentialIssuancePurpose from ...ld_proofs.suites.bbs_bls_signature_2020 import BbsBlsSignature2020 -from ...ld_proofs.suites.bbs_bls_signature_proof_2020 import BbsBlsSignatureProof2020 from ...ld_proofs.suites.ed25519_signature_2018 import Ed25519Signature2018 from ...ld_proofs.suites.ed25519_signature_2020 import Ed25519Signature2020 from ..manager import VcLdpManager, VcLdpManagerError @@ -34,6 +41,10 @@ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1", + { + "ex": "https://example.org/test#", + "test": "ex:test", + }, ], "type": ["VerifiableCredential", "UniversityDegreeCredential"], "credentialSubject": {"test": "key"}, @@ -48,13 +59,21 @@ @pytest.fixture -def wallet(): - yield MagicMock(BaseWallet, autospec=True) +def resolver(): + yield DIDResolver([KeyDIDResolver()]) @pytest.fixture -def profile(): - profile = InMemoryProfile.test_profile() +def profile(resolver: DIDResolver): + profile = InMemoryProfile.test_profile( + {}, + { + DIDMethods: DIDMethods(), + BaseVerificationKeyStrategy: DefaultVerificationKeyStrategy(), + DIDResolver: resolver, + }, + ) + profile.context.injector.bind_instance(DocumentLoader, DocumentLoader(profile)) yield profile @@ -93,10 +112,10 @@ async def test_assert_can_issue_with_id_and_proof_type(manager: VcLdpManager): context.value ) - with async_mock.patch.object( + with mock.patch.object( manager, "_did_info_for_did", - async_mock.CoroutineMock(), + mock.CoroutineMock(), ) as mock_did_info: did_info = DIDInfo( did=TEST_DID_SOV, @@ -136,21 +155,18 @@ async def test_assert_can_issue_with_id_and_proof_type(manager: VcLdpManager): @pytest.mark.asyncio -async def test_get_did_info_for_did_sov(manager: VcLdpManager, wallet: MagicMock): - wallet.get_local_did = async_mock.CoroutineMock() - - did_info = await manager._did_info_for_did(TEST_DID_SOV) - wallet.get_local_did.assert_called_once_with(TEST_DID_SOV.replace("did:sov:", "")) - assert did_info == wallet.get_local_did.return_value - - -@pytest.mark.asyncio -async def test_get_did_info_for_did_key(manager: VcLdpManager, wallet: MagicMock): - wallet.get_local_did.reset_mock() - - did_info = await manager._did_info_for_did(TEST_DID_KEY) - wallet.get_local_did.assert_called_once_with(TEST_DID_KEY) - assert did_info == wallet.get_local_did.return_value +@pytest.mark.parametrize("method", [SOV, KEY]) +async def test_get_did_info_for_did_sov( + method: DIDMethod, profile: Profile, manager: VcLdpManager +): + async with profile.session() as session: + wallet = session.inject(BaseWallet) + did = await wallet.create_local_did( + method=method, + key_type=ED25519, + ) + did_info = await manager._did_info_for_did(did.did) + assert did_info == did @pytest.mark.asyncio @@ -158,14 +174,14 @@ async def test_get_suite_for_credential(manager: VcLdpManager): vc = VerifiableCredential.deserialize(VC["credential"]) options = LDProofVCOptions.deserialize(VC["options"]) - with async_mock.patch.object( + with mock.patch.object( manager, - "_assert_can_issue_with_id_and_proof_type", - async_mock.CoroutineMock(), - ) as mock_can_issue, async_mock.patch.object( + "assert_can_issue_with_id_and_proof_type", + mock.CoroutineMock(), + ) as mock_can_issue, mock.patch.object( manager, "_did_info_for_did", - async_mock.CoroutineMock(), + mock.CoroutineMock(), ) as mock_did_info: suite = await manager._get_suite_for_credential(vc, options) @@ -183,8 +199,8 @@ async def test_get_suite_for_credential(manager: VcLdpManager): @pytest.mark.asyncio async def test_get_suite(manager: VcLdpManager): - proof = async_mock.MagicMock() - did_info = async_mock.MagicMock() + proof = mock.MagicMock() + did_info = mock.MagicMock() suite = await manager._get_suite( proof_type=BbsBlsSignature2020.signature_type, @@ -279,31 +295,75 @@ async def test_prepare_detail_ed25519_2020( @pytest.mark.asyncio -async def test_issue(): - raise NotImplementedError() +async def test_issue( + profile: Profile, + manager: VcLdpManager, + vc: VerifiableCredential, + options: LDProofVCOptions, +): + async with profile.session() as session: + wallet = session.inject(BaseWallet) + did = await wallet.create_local_did( + method=KEY, + key_type=ED25519, + ) + vc.issuer = did.did + options.proof_type = Ed25519Signature2018.signature_type + cred = await manager.issue(vc, options) + assert cred @pytest.mark.asyncio -async def test_issue_ed25519_2020(): +async def test_issue_ed25519_2020( + profile: Profile, + manager: VcLdpManager, + vc: VerifiableCredential, + options: LDProofVCOptions, +): """Ensure ed25519 2020 context added to issued cred.""" - raise NotImplementedError() + async with profile.session() as session: + wallet = session.inject(BaseWallet) + did = await wallet.create_local_did( + method=KEY, + key_type=ED25519, + ) + vc.issuer = did.did + options.proof_type = Ed25519Signature2020.signature_type + cred = await manager.issue(vc, options) + assert cred @pytest.mark.asyncio -async def test_issue_bbs(): +async def test_issue_bbs( + profile: Profile, + manager: VcLdpManager, + vc: VerifiableCredential, + options: LDProofVCOptions, +): """Ensure BBS context is added to issued cred.""" - raise NotImplementedError() + async with profile.session() as session: + wallet = session.inject(BaseWallet) + did = await wallet.create_local_did( + method=KEY, + key_type=BLS12381G2, + ) + vc.issuer = did.did + options.proof_type = BbsBlsSignature2020.signature_type + cred = await manager.issue(vc, options) + assert cred @pytest.mark.asyncio async def test_get_all_suites(manager: VcLdpManager): suites = await manager._get_all_suites() - assert len(suites) == 4 + # An analgous test used to check for BbsBlsSignatureProof2020 + # This is not supported by the VcLdpManager which focuses on + # Issuance and Verification. + assert len(suites) == 3 types = ( Ed25519Signature2018, Ed25519Signature2020, BbsBlsSignature2020, - BbsBlsSignatureProof2020, ) for suite in suites: assert isinstance(suite, types) From 3e0ebeb6e1f2eb31b634e705ed03c51a5cf6f174 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 5 Dec 2023 20:01:49 -0500 Subject: [PATCH 12/16] refactor: signature_type as class var Signed-off-by: Daniel Bluhm --- .../vc/ld_proofs/suites/bbs_bls_signature_2020.py | 2 +- .../vc/ld_proofs/suites/bbs_bls_signature_proof_2020.py | 1 - .../vc/ld_proofs/suites/ed25519_signature_2018.py | 1 - .../vc/ld_proofs/suites/ed25519_signature_2020.py | 1 - .../vc/ld_proofs/suites/jws_linked_data_signature.py | 2 -- aries_cloudagent/vc/ld_proofs/suites/linked_data_proof.py | 6 +++--- .../vc/ld_proofs/suites/linked_data_signature.py | 3 +-- 7 files changed, 5 insertions(+), 11 deletions(-) diff --git a/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py b/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py index af6255b653..233661b88d 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py +++ b/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py @@ -39,7 +39,7 @@ def __init__( date (datetime, optional): Signing date to use. Defaults to now """ - super().__init__(signature_type=BbsBlsSignature2020.signature_type, proof=proof) + super().__init__(proof=proof) self.key_pair = key_pair self.verification_method = verification_method self.date = date diff --git a/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_proof_2020.py b/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_proof_2020.py index 88b00eb317..d9c529b2e3 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_proof_2020.py +++ b/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_proof_2020.py @@ -51,7 +51,6 @@ def __init__( """ super().__init__( - signature_type=BbsBlsSignatureProof2020.signature_type, supported_derive_proof_types=( BbsBlsSignatureProof2020.supported_derive_proof_types ), diff --git a/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2018.py b/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2018.py index d300b78f82..a3fe2a6eef 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2018.py +++ b/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2018.py @@ -32,7 +32,6 @@ def __init__( date (datetime, optional): Signing date to use. """ super().__init__( - signature_type=Ed25519Signature2018.signature_type, algorithm="EdDSA", required_key_type="Ed25519VerificationKey2018", key_pair=key_pair, diff --git a/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2020.py b/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2020.py index 4631b1b671..d0a83b6930 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2020.py +++ b/aries_cloudagent/vc/ld_proofs/suites/ed25519_signature_2020.py @@ -34,7 +34,6 @@ def __init__( date (datetime, optional): Signing date to use. """ super().__init__( - signature_type=Ed25519Signature2020.signature_type, verification_method=verification_method, proof=proof, date=date, diff --git a/aries_cloudagent/vc/ld_proofs/suites/jws_linked_data_signature.py b/aries_cloudagent/vc/ld_proofs/suites/jws_linked_data_signature.py index e93ffdb15d..d20c60b594 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/jws_linked_data_signature.py +++ b/aries_cloudagent/vc/ld_proofs/suites/jws_linked_data_signature.py @@ -22,7 +22,6 @@ class JwsLinkedDataSignature(LinkedDataSignature): def __init__( self, *, - signature_type: str, algorithm: str, required_key_type: str, key_pair: KeyPair, @@ -47,7 +46,6 @@ def __init__( """ super().__init__( - signature_type=signature_type, verification_method=verification_method, proof=proof, date=date, diff --git a/aries_cloudagent/vc/ld_proofs/suites/linked_data_proof.py b/aries_cloudagent/vc/ld_proofs/suites/linked_data_proof.py index 56712a4b7c..d8df7cc535 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/linked_data_proof.py +++ b/aries_cloudagent/vc/ld_proofs/suites/linked_data_proof.py @@ -2,7 +2,7 @@ from abc import ABC -from typing import List, Union +from typing import ClassVar, List, Union from pyld import jsonld from typing_extensions import TypedDict @@ -25,15 +25,15 @@ class DeriveProofResult(TypedDict): class LinkedDataProof(ABC): """Base Linked data proof.""" + signature_type: ClassVar[str] + def __init__( self, *, - signature_type: str, proof: dict = None, supported_derive_proof_types: Union[List[str], None] = None, ): """Initialize new LinkedDataProof instance.""" - self.signature_type = signature_type self.proof = proof self.supported_derive_proof_types = supported_derive_proof_types diff --git a/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py b/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py index bdeb550320..a4547041be 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py +++ b/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py @@ -21,7 +21,6 @@ class LinkedDataSignature(LinkedDataProof, metaclass=ABCMeta): def __init__( self, *, - signature_type: str, proof: dict = None, verification_method: str = None, date: Union[datetime, None] = None, @@ -39,7 +38,7 @@ def __init__( date (datetime, optional): Signing date to use. Defaults to now """ - super().__init__(signature_type=signature_type, proof=proof) + super().__init__(proof=proof) self.verification_method = verification_method self.date = date From 48f20149f3dece62b5955f6b212f21966bd4f7e7 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 5 Dec 2023 20:02:09 -0500 Subject: [PATCH 13/16] fix: bbs signature proof type back in Signed-off-by: Daniel Bluhm --- aries_cloudagent/vc/vc_ld/manager.py | 26 ++++++++++++------- .../vc/vc_ld/tests/test_manager.py | 13 +++++----- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/aries_cloudagent/vc/vc_ld/manager.py b/aries_cloudagent/vc/vc_ld/manager.py index 27b7fbf39f..89f11e9ebc 100644 --- a/aries_cloudagent/vc/vc_ld/manager.py +++ b/aries_cloudagent/vc/vc_ld/manager.py @@ -1,30 +1,30 @@ """Manager for performing Linked Data Proof signatures over JSON-LD formatted W3C VCs.""" -from typing import Optional - -from aries_cloudagent.vc.ld_proofs.constants import ( - SECURITY_CONTEXT_BBS_URL, - SECURITY_CONTEXT_ED25519_2020_URL, -) +from typing import Dict, Optional, Type from ...core.profile import Profile -from ..vc_ld.models.presentation import VerifiablePresentation from ...wallet.base import BaseWallet from ...wallet.default_verification_key_strategy import BaseVerificationKeyStrategy from ...wallet.did_info import DIDInfo from ...wallet.error import WalletNotFoundError -from ...wallet.key_type import BLS12381G2, ED25519 +from ...wallet.key_type import BLS12381G2, ED25519, KeyType +from ..ld_proofs.constants import ( + SECURITY_CONTEXT_BBS_URL, + SECURITY_CONTEXT_ED25519_2020_URL, +) from ..ld_proofs.crypto.wallet_key_pair import WalletKeyPair from ..ld_proofs.document_loader import DocumentLoader from ..ld_proofs.purposes.authentication_proof_purpose import AuthenticationProofPurpose from ..ld_proofs.purposes.credential_issuance_purpose import CredentialIssuancePurpose from ..ld_proofs.purposes.proof_purpose import ProofPurpose from ..ld_proofs.suites.bbs_bls_signature_2020 import BbsBlsSignature2020 +from ..ld_proofs.suites.bbs_bls_signature_proof_2020 import BbsBlsSignatureProof2020 from ..ld_proofs.suites.ed25519_signature_2018 import Ed25519Signature2018 from ..ld_proofs.suites.ed25519_signature_2020 import Ed25519Signature2020 from ..ld_proofs.suites.linked_data_proof import LinkedDataProof from ..ld_proofs.validation_result import DocumentVerificationResult +from ..vc_ld.models.presentation import VerifiablePresentation from ..vc_ld.validation_result import PresentationVerificationResult from .issue import issue as ldp_issue from .models.credential import VerifiableCredential @@ -38,7 +38,7 @@ AuthenticationProofPurpose.term, } SUPPORTED_ISSUANCE_SUITES = {Ed25519Signature2018, Ed25519Signature2020} -SIGNATURE_SUITE_KEY_TYPE_MAPPING = { +SIGNATURE_SUITE_KEY_TYPE_MAPPING: Dict[Type[LinkedDataProof], KeyType] = { Ed25519Signature2018: ED25519, Ed25519Signature2020: ED25519, } @@ -47,7 +47,13 @@ # We only want to add bbs suites to supported if the module is installed if BbsBlsSignature2020.BBS_SUPPORTED: SUPPORTED_ISSUANCE_SUITES.add(BbsBlsSignature2020) - SIGNATURE_SUITE_KEY_TYPE_MAPPING[BbsBlsSignature2020] = BLS12381G2 + SUPPORTED_ISSUANCE_SUITES.add(BbsBlsSignatureProof2020) + SIGNATURE_SUITE_KEY_TYPE_MAPPING.update( + { + BbsBlsSignature2020: BLS12381G2, + BbsBlsSignatureProof2020: BLS12381G2, + } + ) PROOF_TYPE_SIGNATURE_SUITE_MAPPING = { diff --git a/aries_cloudagent/vc/vc_ld/tests/test_manager.py b/aries_cloudagent/vc/vc_ld/tests/test_manager.py index 8264bf456b..33d03450f6 100644 --- a/aries_cloudagent/vc/vc_ld/tests/test_manager.py +++ b/aries_cloudagent/vc/vc_ld/tests/test_manager.py @@ -1,14 +1,13 @@ """Test VcLdpManager.""" import pytest -from aries_cloudagent.resolver.default.key import KeyDIDResolver -from aries_cloudagent.resolver.did_resolver import DIDResolver from aries_cloudagent.tests import mock -from aries_cloudagent.vc.ld_proofs.document_loader import DocumentLoader from ....core.in_memory.profile import InMemoryProfile from ....core.profile import Profile from ....did.did_key import DIDKey +from ....resolver.default.key import KeyDIDResolver +from ....resolver.did_resolver import DIDResolver from ....wallet.base import BaseWallet from ....wallet.default_verification_key_strategy import ( BaseVerificationKeyStrategy, @@ -23,11 +22,13 @@ SECURITY_CONTEXT_ED25519_2020_URL, ) from ...ld_proofs.crypto.wallet_key_pair import WalletKeyPair +from ...ld_proofs.document_loader import DocumentLoader from ...ld_proofs.purposes.authentication_proof_purpose import ( AuthenticationProofPurpose, ) from ...ld_proofs.purposes.credential_issuance_purpose import CredentialIssuancePurpose from ...ld_proofs.suites.bbs_bls_signature_2020 import BbsBlsSignature2020 +from ...ld_proofs.suites.bbs_bls_signature_proof_2020 import BbsBlsSignatureProof2020 from ...ld_proofs.suites.ed25519_signature_2018 import Ed25519Signature2018 from ...ld_proofs.suites.ed25519_signature_2020 import Ed25519Signature2020 from ..manager import VcLdpManager, VcLdpManagerError @@ -356,14 +357,12 @@ async def test_issue_bbs( @pytest.mark.asyncio async def test_get_all_suites(manager: VcLdpManager): suites = await manager._get_all_suites() - # An analgous test used to check for BbsBlsSignatureProof2020 - # This is not supported by the VcLdpManager which focuses on - # Issuance and Verification. - assert len(suites) == 3 + assert len(suites) == 4 types = ( Ed25519Signature2018, Ed25519Signature2020, BbsBlsSignature2020, + BbsBlsSignatureProof2020, ) for suite in suites: assert isinstance(suite, types) From e9dd05d9b501ad01aa8fc95e9f5b0c81937a1824 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 7 Dec 2023 14:07:16 -0500 Subject: [PATCH 14/16] fix: created ts according to xml schema datetime spec Signed-off-by: Daniel Bluhm --- .../vc/ld_proofs/suites/bbs_bls_signature_2020.py | 4 +++- aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py b/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py index 233661b88d..78a820e45f 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py +++ b/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py @@ -63,7 +63,9 @@ async def create_proof( date = self.date or datetime.now(timezone.utc) if not date.tzinfo: date = utc.localize(date) - proof["created"] = date.isoformat() + proof["created"] = ( + date.replace(tzinfo=None).isoformat(timespec="seconds") + "Z" + ) # Allow purpose to update the proof; the `proof` is in the # SECURITY_CONTEXT_URL `@context` -- therefore the `purpose` must diff --git a/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py b/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py index a4547041be..4bca5b7edd 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py +++ b/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py @@ -100,7 +100,9 @@ async def create_proof( date = self.date or datetime.now(timezone.utc) if not date.tzinfo: date = utc.localize(date) - proof["created"] = date.isoformat() + proof["created"] = ( + date.replace(tzinfo=None).isoformat(timespec="seconds") + "Z" + ) # Allow purpose to update the proof; the `proof` is in the # SECURITY_CONTEXT_URL `@context` -- therefore the `purpose` must From 773f806b772167837b0b8cdacce478d45168f85f Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 7 Dec 2023 14:22:47 -0500 Subject: [PATCH 15/16] Revert "fix: created ts according to xml schema datetime spec" This reverts commit fbcbdeb141474c760f878da309e892ee2b070c4f. Signed-off-by: Daniel Bluhm --- .../vc/ld_proofs/suites/bbs_bls_signature_2020.py | 4 +--- aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py b/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py index 78a820e45f..233661b88d 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py +++ b/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py @@ -63,9 +63,7 @@ async def create_proof( date = self.date or datetime.now(timezone.utc) if not date.tzinfo: date = utc.localize(date) - proof["created"] = ( - date.replace(tzinfo=None).isoformat(timespec="seconds") + "Z" - ) + proof["created"] = date.isoformat() # Allow purpose to update the proof; the `proof` is in the # SECURITY_CONTEXT_URL `@context` -- therefore the `purpose` must diff --git a/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py b/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py index 4bca5b7edd..a4547041be 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py +++ b/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py @@ -100,9 +100,7 @@ async def create_proof( date = self.date or datetime.now(timezone.utc) if not date.tzinfo: date = utc.localize(date) - proof["created"] = ( - date.replace(tzinfo=None).isoformat(timespec="seconds") + "Z" - ) + proof["created"] = date.isoformat() # Allow purpose to update the proof; the `proof` is in the # SECURITY_CONTEXT_URL `@context` -- therefore the `purpose` must From d489877bb419a771a1c8d4da4ed8cc8f902d242c Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 7 Dec 2023 15:05:41 -0500 Subject: [PATCH 16/16] fix: no milliseconds in proof created (take 2) Signed-off-by: Daniel Bluhm --- aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py | 2 +- aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py b/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py index 233661b88d..7b2c82401f 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py +++ b/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py @@ -63,7 +63,7 @@ async def create_proof( date = self.date or datetime.now(timezone.utc) if not date.tzinfo: date = utc.localize(date) - proof["created"] = date.isoformat() + proof["created"] = date.isoformat(timespec="seconds") # Allow purpose to update the proof; the `proof` is in the # SECURITY_CONTEXT_URL `@context` -- therefore the `purpose` must diff --git a/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py b/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py index a4547041be..eabc8e86e2 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py +++ b/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py @@ -100,7 +100,7 @@ async def create_proof( date = self.date or datetime.now(timezone.utc) if not date.tzinfo: date = utc.localize(date) - proof["created"] = date.isoformat() + proof["created"] = date.isoformat(timespec="seconds") # Allow purpose to update the proof; the `proof` is in the # SECURITY_CONTEXT_URL `@context` -- therefore the `purpose` must