diff --git a/DIDMethods.md b/DIDMethods.md new file mode 100644 index 0000000000..179103aba8 --- /dev/null +++ b/DIDMethods.md @@ -0,0 +1,45 @@ +# DID methods in ACA-Py +Decentralized Identifiers, or DIDs, are URIs that point to documents that describe cryptographic primitives and protocols used in decentralized identity management. +DIDs include methods that describe where and how documents can be retrieved. +DID methods support specific types of keys and may or may not require the holder to specify the DID itself. + +ACA-Py provides a `DIDMethods` registry holding all the DID methods supported for storage in a wallet + +> :warning: Askar and InMemory are the only wallets supporting this registry. + +## Registering a DID method +By default, ACA-Py supports `did:key` and `did:sov`. +Plugins can register DID additional methods to make them available to holders. +Here's a snippet adding support for `did:web` to the registry from a plugin `setup` method. + +```python= +WEB = DIDMethod( + name="web", + key_types=[ED25519, BLS12381G2], + rotation=True, + holder_defined_did=HolderDefinedDid.REQUIRED # did:web is not derived from key material but from a user-provided respository name +) + +async def setup(context: InjectionContext): + methods = context.inject(DIDMethods) + methods.register(WEB) +``` + +## Creating a DID + +`POST /wallet/did/create` can be provided with parameters for any registered DID method. Here's a follow-up to the +`did:web` method example: + +```json= +{ + "method": "web", + "options": { + "did": "did:web:doma.in", + "key_type": "ed25519" + } +} +``` + +## Resolving DIDs + +For specifics on how DIDs are resolved in ACA-Py, see: [DID Resolution](DIDResolution.md). diff --git a/aries_cloudagent/core/tests/test_conductor.py b/aries_cloudagent/core/tests/test_conductor.py index c105a66307..d37f10161e 100644 --- a/aries_cloudagent/core/tests/test_conductor.py +++ b/aries_cloudagent/core/tests/test_conductor.py @@ -36,7 +36,7 @@ from ...utils.stats import Collector from ...version import __version__ from ...wallet.base import BaseWallet -from ...wallet.did_method import SOV +from ...wallet.did_method import SOV, DIDMethods from ...wallet.key_type import ED25519 from .. import conductor as test_module @@ -87,6 +87,7 @@ async def build_context(self) -> InjectionContext: context.injector.bind_instance(ProfileManager, InMemoryProfileManager()) context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry()) context.injector.bind_instance(BaseWireFormat, self.wire_format) + context.injector.bind_instance(DIDMethods, DIDMethods()) context.injector.bind_instance(DIDResolver, DIDResolver([])) context.injector.bind_instance(EventBus, MockEventBus()) return context diff --git a/aries_cloudagent/ledger/tests/test_indy_vdr.py b/aries_cloudagent/ledger/tests/test_indy_vdr.py index 5f820aeeab..6a575a5737 100644 --- a/aries_cloudagent/ledger/tests/test_indy_vdr.py +++ b/aries_cloudagent/ledger/tests/test_indy_vdr.py @@ -28,7 +28,7 @@ @pytest.fixture() def ledger(): - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile(bind={DIDMethods: DIDMethods()}) ledger = IndyVdrLedger(IndyVdrLedgerPool("test-ledger"), profile) async def open(): diff --git a/aries_cloudagent/messaging/jsonld/tests/test_routes.py b/aries_cloudagent/messaging/jsonld/tests/test_routes.py index 60fb1634d7..ec41daf578 100644 --- a/aries_cloudagent/messaging/jsonld/tests/test_routes.py +++ b/aries_cloudagent/messaging/jsonld/tests/test_routes.py @@ -14,7 +14,7 @@ from ....resolver.did_resolver import DIDResolver from ....vc.ld_proofs.document_loader import DocumentLoader from ....wallet.base import BaseWallet -from ....wallet.did_method import SOV +from ....wallet.did_method import SOV, DIDMethods from ....wallet.error import WalletError from ....wallet.key_type import ED25519 from ..error import ( @@ -274,6 +274,7 @@ async def setUp(self): self.context.profile.context.injector.bind_instance( DocumentLoader, custom_document_loader ) + self.context.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) self.did_info = await (await self.context.session()).wallet.create_local_did( SOV, ED25519 ) diff --git a/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py index 8d6dcc1250..03c7e64647 100644 --- a/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py @@ -23,7 +23,7 @@ from .....storage.error import StorageNotFoundError from .....transport.inbound.receipt import MessageReceipt from .....wallet.base import DIDInfo -from .....wallet.did_method import SOV +from .....wallet.did_method import SOV, DIDMethods from .....wallet.error import WalletNotFoundError from .....wallet.in_memory import InMemoryWallet from .....wallet.key_type import ED25519 @@ -94,6 +94,7 @@ async def setUp(self): BaseCache: InMemoryCache(), OobMessageProcessor: self.oob_mock, RouteManager: self.route_manager, + DIDMethods: DIDMethods(), }, ) self.context = self.profile.context diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_mediation_request_handler.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_mediation_request_handler.py index 61d2b4d449..d3c342ec07 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_mediation_request_handler.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_mediation_request_handler.py @@ -13,6 +13,7 @@ from ...models.mediation_record import MediationRecord from ..mediation_request_handler import MediationRequestHandler +from ......wallet.did_method import DIDMethods TEST_CONN_ID = "conn-id" TEST_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" @@ -24,6 +25,7 @@ class TestMediationRequestHandler(AsyncTestCase): async def setUp(self): """setup dependencies of messaging""" self.context = RequestContext.test_context() + self.context.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) self.session = await self.context.session() self.context.message = MediationRequest() self.context.connection_ready = True diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_mediation_manager.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_mediation_manager.py index 97bc2f80f7..7e93eaa841 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_mediation_manager.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_mediation_manager.py @@ -26,6 +26,7 @@ from ..messages.mediate_grant import MediationGrant from ..messages.mediate_request import MediationRequest from ..models.mediation_record import MediationRecord +from .....wallet.did_method import DIDMethods TEST_CONN_ID = "conn-id" TEST_THREAD_ID = "thread-id" @@ -42,7 +43,9 @@ def profile() -> Iterable[Profile]: """Fixture for profile used in tests.""" # pylint: disable=W0621 - yield InMemoryProfile.test_profile(bind={EventBus: MockEventBus()}) + yield InMemoryProfile.test_profile( + bind={EventBus: MockEventBus(), DIDMethods: DIDMethods()} + ) @pytest.fixture diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py index 4cbd017a45..ababf0ea16 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py @@ -6,11 +6,13 @@ from .....storage.error import StorageError, StorageNotFoundError from ..models.mediation_record import MediationRecord from ..route_manager import RouteManager +from .....wallet.did_method import DIDMethods class TestCoordinateMediationRoutes(AsyncTestCase): def setUp(self): self.profile = InMemoryProfile.test_profile() + self.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) self.context = AdminRequestContext.test_context(profile=self.profile) self.outbound_message_router = async_mock.CoroutineMock() self.request_dict = { diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_complete_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_complete_handler.py index bbeba7c27f..19372e515c 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_complete_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_complete_handler.py @@ -10,11 +10,13 @@ from ...messages.problem_report_reason import ProblemReportReason from .. import complete_handler as test_module +from ......wallet.did_method import DIDMethods @pytest.fixture() def request_context() -> RequestContext: ctx = RequestContext.test_context() + ctx.injector.bind_instance(DIDMethods, DIDMethods()) ctx.message_receipt = MessageReceipt() yield ctx diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py index 95a728310c..279d905863 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py @@ -9,11 +9,13 @@ from ...handlers.invitation_handler import InvitationHandler from ...messages.problem_report_reason import ProblemReportReason +from ......wallet.did_method import DIDMethods @pytest.fixture() def request_context() -> RequestContext: ctx = RequestContext.test_context() + ctx.injector.bind_instance(DIDMethods, DIDMethods()) ctx.message_receipt = MessageReceipt() yield ctx diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py index 8f8c4381b1..6c7a5892b8 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py @@ -9,7 +9,7 @@ Service, ) from ......core.in_memory import InMemoryProfile -from ......wallet.did_method import SOV +from ......wallet.did_method import SOV, DIDMethods from ......wallet.key_type import ED25519 from ......messaging.decorators.attach_decorator import AttachDecorator from ......messaging.request_context import RequestContext @@ -76,6 +76,7 @@ async def setUp(self): "debug.auto_accept_requests_public": True, } ) + self.session.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) self.conn_rec = conn_record.ConnRecord( my_did="55GkHamhTU1ZbTbV2ab9DE", diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py index ff9750ca14..ba81955e36 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py @@ -13,7 +13,7 @@ from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt -from ......wallet.did_method import SOV +from ......wallet.did_method import SOV, DIDMethods from ......wallet.key_type import ED25519 from .....problem_report.v1_0.message import ProblemReport @@ -63,6 +63,7 @@ async def setUp(self): self.ctx = RequestContext.test_context() self.ctx.message_receipt = MessageReceipt() + self.ctx.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) wallet = (await self.ctx.session()).wallet self.did_info = await wallet.create_local_did( method=SOV, diff --git a/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_request.py b/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_request.py index 37569858e6..dc4c8b189e 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_request.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_request.py @@ -5,7 +5,7 @@ from ......connections.models.diddoc import DIDDoc, PublicKey, PublicKeyType, Service from ......core.in_memory import InMemoryProfile from ......messaging.decorators.attach_decorator import AttachDecorator -from ......wallet.did_method import SOV +from ......wallet.did_method import SOV, DIDMethods from ......wallet.key_type import ED25519 from .....didcomm_prefix import DIDCommPrefix from ...message_types import DIDX_REQUEST @@ -49,7 +49,9 @@ def make_did_doc(self): class TestDIDXRequest(AsyncTestCase, TestConfig): async def setUp(self): - self.wallet = InMemoryProfile.test_session().wallet + self.session = InMemoryProfile.test_session() + self.session.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) + self.wallet = self.session.wallet self.did_info = await self.wallet.create_local_did( method=SOV, key_type=ED25519, @@ -106,7 +108,9 @@ class TestDIDXRequestSchema(AsyncTestCase, TestConfig): """Test request schema.""" async def setUp(self): - self.wallet = InMemoryProfile.test_session().wallet + self.session = InMemoryProfile.test_session() + self.session.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) + self.wallet = self.session.wallet self.did_info = await self.wallet.create_local_did( method=SOV, key_type=ED25519, diff --git a/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_response.py b/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_response.py index 59657ef2dc..3be60ca51e 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_response.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_response.py @@ -5,7 +5,7 @@ from ......connections.models.diddoc import DIDDoc, PublicKey, PublicKeyType, Service from ......core.in_memory import InMemoryProfile from ......messaging.decorators.attach_decorator import AttachDecorator -from ......wallet.did_method import SOV +from ......wallet.did_method import SOV, DIDMethods from ......wallet.key_type import ED25519 from .....didcomm_prefix import DIDCommPrefix from ...message_types import DIDX_RESPONSE @@ -48,7 +48,10 @@ def make_did_doc(self): class TestDIDXResponse(AsyncTestCase, TestConfig): async def setUp(self): - self.wallet = InMemoryProfile.test_session().wallet + self.session = InMemoryProfile.test_session() + self.session.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) + self.wallet = self.session.wallet + self.did_info = await self.wallet.create_local_did( method=SOV, key_type=ED25519, @@ -102,7 +105,10 @@ class TestDIDXResponseSchema(AsyncTestCase, TestConfig): """Test response schema.""" async def setUp(self): - self.wallet = InMemoryProfile.test_session().wallet + self.session = InMemoryProfile.test_session() + self.session.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) + self.wallet = self.session.wallet + self.did_info = await self.wallet.create_local_did( method=SOV, key_type=ED25519, diff --git a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py index 05641b86a4..30ae59db04 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py @@ -24,7 +24,7 @@ from .....storage.error import StorageNotFoundError from .....transport.inbound.receipt import MessageReceipt from .....wallet.did_info import DIDInfo -from .....wallet.did_method import SOV +from .....wallet.did_method import SOV, DIDMethods from .....wallet.error import WalletError from .....wallet.in_memory import InMemoryWallet from .....wallet.key_type import ED25519 @@ -102,6 +102,7 @@ async def setUp(self): BaseCache: InMemoryCache(), OobMessageProcessor: self.oob_mock, RouteManager: self.route_manager, + DIDMethods: DIDMethods(), }, ) self.context = self.profile.context diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_manager.py index 1ce20b7ca5..f4b2719237 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_manager.py @@ -12,7 +12,7 @@ from .....ledger.base import BaseLedger from .....storage.error import StorageNotFoundError from .....wallet.base import BaseWallet -from .....wallet.did_method import SOV +from .....wallet.did_method import SOV, DIDMethods from .....wallet.key_type import ED25519 from ..manager import TransactionManager, TransactionManagerError from ..models.transaction_record import TransactionRecord @@ -112,6 +112,7 @@ async def setUp(self): self.profile = self.context.profile injector = self.profile.context.injector injector.bind_instance(BaseLedger, self.ledger) + injector.bind_instance(DIDMethods, DIDMethods()) async with self.profile.session() as session: self.wallet: BaseWallet = session.inject_or(BaseWallet) diff --git a/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch_handler.py b/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch_handler.py index 72525e2fe8..87597359cc 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch_handler.py @@ -15,7 +15,7 @@ from .....storage.vc_holder.vc_record import VCRecord from .....wallet.base import BaseWallet, DIDInfo from .....wallet.crypto import KeyType -from .....wallet.did_method import SOV, KEY +from .....wallet.did_method import SOV, KEY, DIDMethods from .....wallet.error import WalletNotFoundError from .....vc.ld_proofs import ( BbsBlsSignature2020, @@ -69,7 +69,7 @@ def event_loop(request): @pytest.fixture(scope="class") def profile(): - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile(bind={DIDMethods: DIDMethods()}) context = profile.context context.injector.bind_instance(DIDResolver, DIDResolver([])) context.injector.bind_instance(DocumentLoader, custom_document_loader) diff --git a/aries_cloudagent/transport/tests/test_pack_format.py b/aries_cloudagent/transport/tests/test_pack_format.py index 02a7712e1c..764e4a54bf 100644 --- a/aries_cloudagent/transport/tests/test_pack_format.py +++ b/aries_cloudagent/transport/tests/test_pack_format.py @@ -8,7 +8,7 @@ from ...protocols.didcomm_prefix import DIDCommPrefix from ...protocols.routing.v1_0.message_types import FORWARD from ...wallet.base import BaseWallet -from ...wallet.did_method import SOV +from ...wallet.did_method import SOV, DIDMethods from ...wallet.error import WalletError from ...wallet.key_type import ED25519 from .. import pack_format as test_module @@ -33,6 +33,7 @@ class TestPackWireFormat(AsyncTestCase): def setUp(self): self.session = InMemoryProfile.test_session() + self.session.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) self.wallet = self.session.inject(BaseWallet) async def test_errors(self): diff --git a/aries_cloudagent/wallet/askar.py b/aries_cloudagent/wallet/askar.py index 5d84df9f26..2a0093d276 100644 --- a/aries_cloudagent/wallet/askar.py +++ b/aries_cloudagent/wallet/askar.py @@ -15,9 +15,9 @@ SeedMethod, ) +from .did_parameters_validation import DIDParametersValidation from ..askar.didcomm.v1 import pack_message, unpack_message from ..askar.profile import AskarProfileSession -from ..did.did_key import DIDKey from ..ledger.base import BaseLedger from ..ledger.endpoint_type import EndpointType from ..ledger.error import LedgerConfigError @@ -30,7 +30,7 @@ validate_seed, verify_signed_message, ) -from .did_method import SOV, KEY, DIDMethod, DIDMethods +from .did_method import SOV, DIDMethod, DIDMethods from .error import WalletError, WalletDuplicateError, WalletNotFoundError from .key_type import BLS12381G2, ED25519, KeyType, KeyTypes from .util import b58_to_bytes, bytes_to_b58 @@ -171,29 +171,23 @@ async def create_local_did( WalletError: If there is another backend error """ - - # validate key_type - if not method.supports_key_type(key_type): - raise WalletError( - f"Invalid key type {key_type.key_type}" - f" for DID method {method.method_name}" - ) - - if method == KEY and did: - raise WalletError("Not allowed to set DID for DID method 'key'") + did_validation = DIDParametersValidation( + self._session.context.inject(DIDMethods) + ) + did_validation.validate_key_type(method, key_type) if not metadata: metadata = {} - if method not in [SOV, KEY]: - raise WalletError( - f"Unsupported DID method for askar storage: {method.method_name}" - ) try: keypair = _create_keypair(key_type, seed) verkey_bytes = keypair.get_public_bytes() verkey = bytes_to_b58(verkey_bytes) + did = did_validation.validate_or_derive_did( + method, key_type, verkey_bytes, did + ) + try: await self._session.handle.insert_key( verkey, keypair, metadata=json.dumps(metadata) @@ -205,11 +199,6 @@ async def create_local_did( else: raise WalletError("Error inserting key") from err - if method == KEY: - did = DIDKey.from_public_key(verkey_bytes, key_type).did - elif not did: - did = bytes_to_b58(verkey_bytes[:16]) - item = await self._session.handle.fetch(CATEGORY_DID, did, for_update=True) if item: did_info = item.value_json diff --git a/aries_cloudagent/wallet/did_method.py b/aries_cloudagent/wallet/did_method.py index af971ea019..cbe1361b39 100644 --- a/aries_cloudagent/wallet/did_method.py +++ b/aries_cloudagent/wallet/did_method.py @@ -1,19 +1,35 @@ """did method.py contains registry for did methods.""" +from enum import Enum from typing import Dict, List, Mapping, Optional from .error import BaseError from .key_type import BLS12381G2, ED25519, KeyType +class HolderDefinedDid(Enum): + """Define if a holder can specify its own did for a given method.""" + + NO = "no" # holder CANNOT provide a DID + ALLOWED = "allowed" # holder CAN provide a DID + REQUIRED = "required" # holder MUST provide a DID + + class DIDMethod: """Class to represent a did method.""" - def __init__(self, name: str, key_types: List[KeyType], rotation: bool = False): + def __init__( + self, + name: str, + key_types: List[KeyType], + rotation: bool = False, + holder_defined_did: HolderDefinedDid = HolderDefinedDid.NO, + ): """Construct did method class.""" self._method_name: str = name self._supported_key_types: List[KeyType] = key_types self._supports_rotation: bool = rotation + self._holder_defined_did: HolderDefinedDid = holder_defined_did @property def method_name(self): @@ -34,8 +50,21 @@ def supports_key_type(self, key_type: KeyType) -> bool: """Check whether the current method supports the key type.""" return key_type in self.supported_key_types + def holder_defined_did(self) -> HolderDefinedDid: + """Return the did derivation policy. -SOV = DIDMethod(name="sov", key_types=[ED25519], rotation=True) + eg: did:key DIDs are derived from the verkey -> HolderDefinedDid.NO + eg: did:web DIDs cannot be derived from key material -> HolderDefinedDid.REQUIRED + """ + return self._holder_defined_did + + +SOV = DIDMethod( + name="sov", + key_types=[ED25519], + rotation=True, + holder_defined_did=HolderDefinedDid.ALLOWED, +) KEY = DIDMethod( name="key", key_types=[ED25519, BLS12381G2], @@ -55,7 +84,7 @@ def __init__(self) -> None: def registered(self, method: str) -> bool: """Check for a supported method.""" - return method in list(self._registry.items()) + return method in self._registry.keys() def register(self, method: DIDMethod): """Register a new did method.""" diff --git a/aries_cloudagent/wallet/did_parameters_validation.py b/aries_cloudagent/wallet/did_parameters_validation.py new file mode 100644 index 0000000000..04572c77bf --- /dev/null +++ b/aries_cloudagent/wallet/did_parameters_validation.py @@ -0,0 +1,65 @@ +"""Tooling to validate DID creation parameters.""" + +from typing import Optional + +from aries_cloudagent.did.did_key import DIDKey +from aries_cloudagent.wallet.did_method import ( + DIDMethods, + DIDMethod, + HolderDefinedDid, + KEY, + SOV, +) +from aries_cloudagent.wallet.error import WalletError +from aries_cloudagent.wallet.key_type import KeyType +from aries_cloudagent.wallet.util import bytes_to_b58 + + +class DIDParametersValidation: + """A utility class to check compatibility of provided DID creation parameters.""" + + def __init__(self, did_methods: DIDMethods): + """:param did_methods: DID method registry relevant for the validation.""" + self.did_methods = did_methods + + @staticmethod + def validate_key_type(method: DIDMethod, key_type: KeyType): + """Validate compatibility of the DID method with the desired key type.""" + # validate key_type + if not method.supports_key_type(key_type): + raise WalletError( + f"Invalid key type {key_type.key_type}" + f" for DID method {method.method_name}" + ) + + def validate_or_derive_did( + self, + method: DIDMethod, + key_type: KeyType, + verkey: bytes, + did: Optional[str], + ) -> str: + """ + Validate compatibility of the provided did (if any) with the given DID method. + + If no DID was provided, automatically derive one for methods that support it. + """ + if method.holder_defined_did() == HolderDefinedDid.NO and did: + raise WalletError( + f"Not allowed to set DID for DID method '{method.method_name}'" + ) + elif method.holder_defined_did() == HolderDefinedDid.REQUIRED and not did: + raise WalletError(f"Providing a DID is required {method.method_name}") + elif not self.did_methods.registered(method.method_name): + raise WalletError( + f"Unsupported DID method for current storage: {method.method_name}" + ) + + # We need some did method specific handling. If more did methods + # are added it is probably better create a did method specific handler + elif method == KEY: + return DIDKey.from_public_key(verkey, key_type).did + elif method == SOV: + return bytes_to_b58(verkey[:16]) if not did else did + + return did diff --git a/aries_cloudagent/wallet/in_memory.py b/aries_cloudagent/wallet/in_memory.py index f9bb03a37f..5023e8d6c0 100644 --- a/aries_cloudagent/wallet/in_memory.py +++ b/aries_cloudagent/wallet/in_memory.py @@ -3,8 +3,8 @@ import asyncio from typing import List, Sequence, Tuple, Union +from .did_parameters_validation import DIDParametersValidation from ..core.in_memory import InMemoryProfile -from ..did.did_key import DIDKey from .base import BaseWallet from .crypto import ( @@ -17,7 +17,7 @@ ) from .did_info import KeyInfo, DIDInfo from .did_posture import DIDPosture -from .did_method import SOV, KEY, DIDMethod, DIDMethods +from .did_method import SOV, DIDMethod, DIDMethods from .error import WalletError, WalletDuplicateError, WalletNotFoundError from .key_type import KeyType from .util import b58_to_bytes, bytes_to_b58, random_seed @@ -212,27 +212,15 @@ async def create_local_did( """ seed = validate_seed(seed) or random_seed() - # validate key_type - if not method.supports_key_type(key_type): - raise WalletError( - f"Invalid key type {key_type.key_type} for method {method.method_name}" - ) + did_methods: DIDMethods = self.profile.context.inject(DIDMethods) + did_validation = DIDParametersValidation(did_methods) + + did_validation.validate_key_type(method, key_type) verkey, secret = create_keypair(key_type, seed) verkey_enc = bytes_to_b58(verkey) - # We need some did method specific handling. If more did methods - # are added it is probably better create a did method specific handler - if method == KEY: - if did: - raise WalletError("Not allowed to set DID for DID method 'key'") - - did = DIDKey.from_public_key(verkey, key_type).did - elif method == SOV: - if not did: - did = bytes_to_b58(verkey[:16]) - else: - raise WalletError(f"Unsupported DID method: {method.method_name}") + did = did_validation.validate_or_derive_did(method, key_type, verkey, did) if ( did in self.profile.local_dids diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 1c038957dc..3cd204f66b 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -25,6 +25,7 @@ INDY_DID, INDY_OR_KEY_DID, INDY_RAW_PUBLIC_KEY, + GENERIC_DID, ) from ..protocols.coordinate_mediation.v1_0.route_manager import RouteManager from ..protocols.endorse_transaction.v1_0.manager import ( @@ -38,7 +39,7 @@ from ..storage.error import StorageError, StorageNotFoundError from .base import BaseWallet from .did_info import DIDInfo -from .did_method import SOV, KEY, DIDMethod, DIDMethods +from .did_method import SOV, KEY, DIDMethod, DIDMethods, HolderDefinedDid from .did_posture import DIDPosture from .error import WalletError, WalletNotFoundError from .key_type import BLS12381G2, ED25519, KeyTypes @@ -119,7 +120,7 @@ class DIDEndpointSchema(OpenAPISchema): class DIDListQueryStringSchema(OpenAPISchema): """Parameters and validators for DID list request query string.""" - did = fields.Str(description="DID of interest", required=False, **INDY_OR_KEY_DID) + did = fields.Str(description="DID of interest", required=False, **GENERIC_DID) verkey = fields.Str( description="Verification key of interest", required=False, @@ -160,9 +161,18 @@ class DIDCreateOptionsSchema(OpenAPISchema): key_type = fields.Str( required=True, example=ED25519.key_type, + description="Key type to use for the DID keypair. " + + "Validated with the chosen DID method's supported key types.", validate=validate.OneOf([ED25519.key_type, BLS12381G2.key_type]), ) + did = fields.Str( + required=False, + description="Specify final value of the did (including did:: prefix)" + + "if the method supports or requires so.", + **GENERIC_DID, + ) + class DIDCreateSchema(OpenAPISchema): """Parameters and validators for create DID endpoint.""" @@ -171,13 +181,14 @@ class DIDCreateSchema(OpenAPISchema): required=False, default=SOV.method_name, example=SOV.method_name, - validate=validate.OneOf([KEY.method_name, SOV.method_name]), + description="Method for the requested DID." + + "Supported methods are 'key', 'sov', and any other registered method.", ) options = fields.Nested( DIDCreateOptionsSchema, required=False, - description="To define a key type for a did:key", + description="To define a key type and/or a did depending on chosen DID method.", ) seed = fields.Str( @@ -374,14 +385,26 @@ async def wallet_create_did(request: web.BaseRequest): f" support key type {key_type.key_type}" ) ) + + did = body.get("options", {}).get("did") + if method.holder_defined_did() == HolderDefinedDid.NO and did: + raise web.HTTPForbidden( + reason=( + f"method {method.method_name} does not" + f" support user-defined DIDs" + ) + ) + elif method.holder_defined_did() == HolderDefinedDid.REQUIRED and not did: + raise web.HTTPBadRequest( + reason=f"method {method.method_name} requires a user-defined DIDs" + ) + wallet = session.inject_or(BaseWallet) if not wallet: raise web.HTTPForbidden(reason="No wallet available") try: info = await wallet.create_local_did( - method=method, - key_type=key_type, - seed=seed, + method=method, key_type=key_type, seed=seed, did=did ) except WalletError as err: diff --git a/aries_cloudagent/wallet/tests/test_did_parameters_validation.py b/aries_cloudagent/wallet/tests/test_did_parameters_validation.py new file mode 100644 index 0000000000..73565f827f --- /dev/null +++ b/aries_cloudagent/wallet/tests/test_did_parameters_validation.py @@ -0,0 +1,83 @@ +import pytest + +from aries_cloudagent.wallet.did_method import DIDMethods, DIDMethod, HolderDefinedDid +from aries_cloudagent.wallet.did_parameters_validation import DIDParametersValidation +from aries_cloudagent.wallet.error import WalletError +from aries_cloudagent.wallet.key_type import ED25519, BLS12381G1 + + +@pytest.fixture +def did_methods_registry(): + return DIDMethods() + + +def test_validate_key_type_uses_didmethod_when_validating_key_type( + did_methods_registry, +): + # given + ed_method = DIDMethod("ed-method", [ED25519]) + did_methods_registry.register(ed_method) + did_validation = DIDParametersValidation(did_methods_registry) + + # when - then + assert did_validation.validate_key_type(ed_method, ED25519) is None + with pytest.raises(WalletError): + did_validation.validate_key_type(ed_method, BLS12381G1) + + +def test_validate_key_type_raises_exception_when_validating_unknown_did_method( + did_methods_registry, +): + # given + unknown_method = DIDMethod("unknown", []) + did_validation = DIDParametersValidation(did_methods_registry) + + # when - then + with pytest.raises(WalletError): + did_validation.validate_key_type(unknown_method, ED25519) + + +def test_set_did_raises_error_when_did_is_provided_and_method_doesnt_allow( + did_methods_registry, +): + # given + ed_method = DIDMethod( + "derived-did", [ED25519], holder_defined_did=HolderDefinedDid.NO + ) + did_methods_registry.register(ed_method) + did_validation = DIDParametersValidation(did_methods_registry) + + # when - then + with pytest.raises(WalletError): + did_validation.validate_or_derive_did( + ed_method, ED25519, b"verkey", "did:edward:self-defined" + ) + + +def test_validate_or_derive_did_raises_error_when_no_did_is_provided_and_method_requires_one( + did_methods_registry, +): + # given + ed_method = DIDMethod( + "self-defined-did", [ED25519], holder_defined_did=HolderDefinedDid.REQUIRED + ) + did_methods_registry.register(ed_method) + did_validation = DIDParametersValidation(did_methods_registry) + + # when - then + with pytest.raises(WalletError): + did_validation.validate_or_derive_did(ed_method, ED25519, b"verkey", did=None) + + +def test_validate_or_derive_did_raises_exception_when_validating_unknown_did_method( + did_methods_registry, +): + # given + unknown_method = DIDMethod("unknown", []) + did_validation = DIDParametersValidation(did_methods_registry) + + # when - then + with pytest.raises(WalletError): + did_validation.validate_or_derive_did( + unknown_method, ED25519, b"verkey", did=None + ) diff --git a/aries_cloudagent/wallet/tests/test_in_memory_wallet.py b/aries_cloudagent/wallet/tests/test_in_memory_wallet.py index ecb3c97263..e9d81ffa27 100644 --- a/aries_cloudagent/wallet/tests/test_in_memory_wallet.py +++ b/aries_cloudagent/wallet/tests/test_in_memory_wallet.py @@ -13,6 +13,7 @@ @pytest.fixture() async def wallet(): profile = InMemoryProfile.test_profile() + profile.context.injector.bind_instance(DIDMethods, DIDMethods()) wallet = InMemoryWallet(profile) yield wallet diff --git a/aries_cloudagent/wallet/tests/test_indy_wallet.py b/aries_cloudagent/wallet/tests/test_indy_wallet.py index 8dfe821e58..47ae72cdec 100644 --- a/aries_cloudagent/wallet/tests/test_indy_wallet.py +++ b/aries_cloudagent/wallet/tests/test_indy_wallet.py @@ -17,7 +17,7 @@ from ...indy.sdk.wallet_setup import IndyWalletConfig from ...ledger.endpoint_type import EndpointType from ...ledger.indy import IndySdkLedgerPool -from ...wallet.did_method import SOV +from ...wallet.did_method import SOV, DIDMethods from ...wallet.key_type import ED25519 from .. import indy as test_module from ..base import BaseWallet @@ -28,7 +28,7 @@ @pytest.fixture() async def in_memory_wallet(): - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile(bind={DIDMethods: DIDMethods()}) wallet = InMemoryWallet(profile) yield wallet @@ -38,6 +38,7 @@ async def wallet(): key = await IndySdkWallet.generate_wallet_key() context = InjectionContext() context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) + context.injector.bind_instance(DIDMethods, DIDMethods()) with async_mock.patch.object(IndySdkProfile, "_make_finalizer"): profile = cast( IndySdkProfile, diff --git a/aries_cloudagent/wallet/tests/test_routes.py b/aries_cloudagent/wallet/tests/test_routes.py index 75abc4d483..03c1dc57cb 100644 --- a/aries_cloudagent/wallet/tests/test_routes.py +++ b/aries_cloudagent/wallet/tests/test_routes.py @@ -1,4 +1,5 @@ import mock as async_mock +import pytest from aiohttp.web import HTTPForbidden from async_case import IsolatedAsyncioTestCase @@ -6,7 +7,7 @@ from ...core.in_memory import InMemoryProfile from ...ledger.base import BaseLedger from ...protocols.coordinate_mediation.v1_0.route_manager import RouteManager -from ...wallet.did_method import SOV, DIDMethods +from ...wallet.did_method import SOV, DIDMethods, DIDMethod, HolderDefinedDid from ...wallet.key_type import ED25519, KeyTypes from .. import routes as test_module from ..base import BaseWallet @@ -38,7 +39,8 @@ def setUp(self): self.test_verkey = "verkey" self.test_posted_did = "posted-did" self.test_posted_verkey = "posted-verkey" - self.context.injector.bind_instance(DIDMethods, DIDMethods()) + self.did_methods = DIDMethods() + self.context.injector.bind_instance(DIDMethods, self.did_methods) async def test_missing_wallet(self): self.session_inject[BaseWallet] = None @@ -134,6 +136,48 @@ async def test_create_did_unsupported_key_type(self): with self.assertRaises(test_module.web.HTTPForbidden): await test_module.wallet_create_did(self.request) + async def test_create_did_method_requires_user_defined_did(self): + # given + did_custom = DIDMethod( + name="custom", + key_types=[ED25519], + rotation=True, + holder_defined_did=HolderDefinedDid.REQUIRED, + ) + self.did_methods.register(did_custom) + + self.request.json = async_mock.AsyncMock( + return_value={"method": "custom", "options": {"key_type": "ed25519"}} + ) + + # when - then + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.wallet_create_did(self.request) + + async def test_create_did_method_doesnt_support_user_defined_did(self): + did_custom = DIDMethod( + name="custom", + key_types=[ED25519], + rotation=True, + holder_defined_did=HolderDefinedDid.NO, + ) + self.did_methods.register(did_custom) + + # when + self.request.json = async_mock.AsyncMock( + return_value={ + "method": "custom", + "options": { + "key_type": ED25519.key_type, + "did": "did:custom:aCustomUserDefinedDID", + }, + } + ) + + # then + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.wallet_create_did(self.request) + async def test_create_did_x(self): self.wallet.create_local_did.side_effect = test_module.WalletError() with self.assertRaises(test_module.web.HTTPBadRequest):