Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: emit did:peer:2 in DID Exchange #2578

10 changes: 10 additions & 0 deletions aries_cloudagent/config/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,12 @@ def add_arguments(self, parser: ArgumentParser):
"using unencrypted rather than encrypted tags"
),
)
parser.add_argument(
"--emit-did-peer-2",
action="store_true",
env_var="ACAPY_EMIT_DID_PEER_2",
help=("Emit did:peer:2 DIDs in DID Exchange Protocol"),
)

def get_settings(self, args: Namespace) -> dict:
"""Get protocol settings."""
Expand Down Expand Up @@ -1234,6 +1240,10 @@ def get_settings(self, args: Namespace) -> dict:
if args.exch_use_unencrypted_tags:
settings["exch_use_unencrypted_tags"] = True
environ["EXCH_UNENCRYPTED_TAGS"] = "True"

if args.emit_did_peer_2:
settings["emit_did_peer_2"] = True

return settings


Expand Down
77 changes: 75 additions & 2 deletions aries_cloudagent/connections/base_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import logging
from typing import List, Optional, Sequence, Text, Tuple, Union

from base58 import b58decode
from did_peer_2 import KeySpec, generate
from pydid import (
BaseDIDDocument as ResolvedDocument,
DIDCommService,
Expand All @@ -17,6 +19,7 @@
Ed25519VerificationKey2018,
Ed25519VerificationKey2020,
JsonWebKey2020,
Multikey,
)

from ..cache.base import BaseCache
Expand Down Expand Up @@ -44,8 +47,8 @@
from ..utils.multiformats import multibase, multicodec
from ..wallet.base import BaseWallet
from ..wallet.crypto import create_keypair, seed_to_did
from ..wallet.did_info import DIDInfo
from ..wallet.did_method import SOV
from ..wallet.did_info import DIDInfo, KeyInfo
from ..wallet.did_method import PEER2, SOV
from ..wallet.error import WalletNotFoundError
from ..wallet.key_type import ED25519
from ..wallet.util import b64_to_bytes, bytes_to_b58
Expand Down Expand Up @@ -74,6 +77,69 @@ def __init__(self, profile: Profile):
self._route_manager = profile.inject(RouteManager)
self._logger = logging.getLogger(__name__)

@staticmethod
def _key_info_to_multikey(key_info: KeyInfo) -> str:
"""Convert a KeyInfo to a multikey."""
return multibase.encode(
multicodec.wrap("ed25519-pub", b58decode(key_info.verkey)), "base58btc"
)

async def create_did_peer_2(
self,
svc_endpoints: Optional[Sequence[str]] = None,
mediation_records: Optional[List[MediationRecord]] = None,
) -> DIDInfo:
"""Create a did:peer:2 DID for a connection.

Args:
svc_endpoints: Custom endpoints for the DID Document
mediation_record: The record for mediation that contains routing_keys and
service endpoint

Returns:
The new `DIDInfo` instance
"""
routing_keys: List[str] = []
if mediation_records:
for mediation_record in mediation_records:
(
mediator_routing_keys,
endpoint,
) = await self._route_manager.routing_info(
self._profile, mediation_record
)
routing_keys = [*routing_keys, *(mediator_routing_keys or [])]
if endpoint:
svc_endpoints = [endpoint]

services = []
for index, endpoint in enumerate(svc_endpoints or []):
services.append(
{
"id": f"#didcomm-{index}",
"type": "did-communication",
"priority": index,
"recipientKeys": ["#key-1"],
"routingKeys": routing_keys,
"serviceEndpoint": endpoint,
}
)

async with self._profile.session() as session:
wallet = session.inject(BaseWallet)
key = await wallet.create_key(ED25519)

did = generate(
[KeySpec.verification(self._key_info_to_multikey(key))], services
)

did_info = DIDInfo(
did=did, method=PEER2, verkey=key.verkey, metadata={}, key_type=ED25519
)
await wallet.store_did(did_info)

return did_info

async def create_did_document(
self,
did_info: DIDInfo,
Expand Down Expand Up @@ -375,6 +441,13 @@ def _extract_key_material_in_base58_format(method: VerificationMethod) -> str:
f"Key type {type(method).__name__} "
f"with kty {method.public_key_jwk.get('kty')} is not supported"
)
elif isinstance(method, Multikey):
codec, key = multicodec.unwrap(multibase.decode(method.material))
if codec != multicodec.multicodec("ed25519-pub"):
raise BaseConnectionManagerError(
"Expected ed25519 multicodec, got: %s", codec
)
return bytes_to_b58(key)
else:
raise BaseConnectionManagerError(
f"Key type {type(method).__name__} is not supported"
Expand Down
34 changes: 34 additions & 0 deletions aries_cloudagent/messaging/decorators/attach_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,40 @@ def content(self) -> Union[Mapping, Tuple[Sequence[str], str]]:
else:
return None

@classmethod
def data_base64_string(
cls,
content: str,
*,
ident: str = None,
description: str = None,
filename: str = None,
lastmod_time: str = None,
byte_count: int = None,
):
"""Create `AttachDecorator` instance on base64-encoded string data.

Given string content, base64-encode, and embed it as data; mark
`text/string` MIME type.

Args:
content: string content
ident: optional attachment identifier (default random UUID4)
description: optional attachment description
filename: optional attachment filename
lastmod_time: optional attachment last modification time
byte_count: optional attachment byte count
"""
return AttachDecorator(
ident=ident or str(uuid.uuid4()),
description=description,
filename=filename,
mime_type="text/string",
lastmod_time=lastmod_time,
byte_count=byte_count,
data=AttachDecoratorData(base64_=bytes_to_b64(content.encode())),
)

@classmethod
def data_base64(
cls,
Expand Down
99 changes: 74 additions & 25 deletions aries_cloudagent/protocols/didexchange/v1_0/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,24 @@ async def create_request(

my_info = None

# Create connection request message
if my_endpoint:
my_endpoints = [my_endpoint]
else:
my_endpoints = []
default_endpoint = self.profile.settings.get("default_endpoint")
if default_endpoint:
my_endpoints.append(default_endpoint)
my_endpoints.extend(self.profile.settings.get("additional_endpoints", []))

emit_did_peer_2 = self.profile.settings.get("emit_did_peer_2")
if conn_rec.my_did:
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
my_info = await wallet.get_local_did(conn_rec.my_did)
elif emit_did_peer_2:
my_info = await self.create_did_peer_2(my_endpoints, mediation_records)
conn_rec.my_did = my_info.did
else:
# Create new DID for connection
async with self.profile.session() as session:
Expand All @@ -308,17 +322,7 @@ async def create_request(
)
conn_rec.my_did = my_info.did

# Create connection request message
if my_endpoint:
my_endpoints = [my_endpoint]
else:
my_endpoints = []
default_endpoint = self.profile.settings.get("default_endpoint")
if default_endpoint:
my_endpoints.append(default_endpoint)
my_endpoints.extend(self.profile.settings.get("additional_endpoints", []))

if use_public_did:
if use_public_did or emit_did_peer_2:
# Omit DID Doc attachment if we're using a public DID
did_doc = None
attach = None
Expand Down Expand Up @@ -605,6 +609,16 @@ async def create_response(
async with self.profile.session() as session:
request = await conn_rec.retrieve_request(session)

if my_endpoint:
my_endpoints = [my_endpoint]
else:
my_endpoints = []
default_endpoint = self.profile.settings.get("default_endpoint")
if default_endpoint:
my_endpoints.append(default_endpoint)
my_endpoints.extend(self.profile.settings.get("additional_endpoints", []))

emit_did_peer_2 = self.profile.settings.get("emit_did_peer_2")
if conn_rec.my_did:
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
Expand All @@ -620,6 +634,10 @@ async def create_response(
did = my_info.did
if not did.startswith("did:"):
did = f"did:sov:{did}"
elif emit_did_peer_2:
my_info = await self.create_did_peer_2(my_endpoints, mediation_records)
conn_rec.my_did = my_info.did
did = my_info.did
else:
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
Expand All @@ -635,20 +653,17 @@ async def create_response(
self.profile, conn_rec, mediation_records
)

# Create connection response message
if my_endpoint:
my_endpoints = [my_endpoint]
else:
my_endpoints = []
default_endpoint = self.profile.settings.get("default_endpoint")
if default_endpoint:
my_endpoints.append(default_endpoint)
my_endpoints.extend(self.profile.settings.get("additional_endpoints", []))

if use_public_did:
if use_public_did or emit_did_peer_2:
# Omit DID Doc attachment if we're using a public DID
did_doc = None
attach = None
attach = AttachDecorator.data_base64_string(did)
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
if conn_rec.invitation_key is not None:
await attach.data.sign(conn_rec.invitation_key, wallet)
else:
self._logger.warning("Invitation key was not set for connection")
attach = None
response = DIDXResponse(did=did, did_rotate_attach=attach)
else:
did_doc = await self.create_did_document(
my_info,
Expand All @@ -659,8 +674,8 @@ async def create_response(
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
await attach.data.sign(conn_rec.invitation_key, wallet)
response = DIDXResponse(did=did, did_doc_attach=attach)

response = DIDXResponse(did=did, did_doc_attach=attach)
# Assign thread information
response.assign_thread_from(request)
response.assign_trace_from(request)
Expand Down Expand Up @@ -783,6 +798,23 @@ async def accept_response(
if response.did is None:
raise DIDXManagerError("No DID in response")

if response.did_rotate_attach is None:
raise DIDXManagerError(
"did_rotate~attach required if no signed doc attachment"
)

self._logger.debug("did_rotate~attach found; verifying signature")
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
signed_did = await self.verify_rotate(
wallet, response.did_rotate_attach, conn_rec.invitation_key
)
if their_did != response.did:
raise DIDXManagerError(
f"Connection DID {their_did} "
f"does not match singed DID rotate {signed_did}"
)

self._logger.debug(
"No DID Doc attachment in response; doc will be resolved from DID"
)
Expand Down Expand Up @@ -955,6 +987,23 @@ async def verify_diddoc(

return json.loads(signed_diddoc_bytes.decode())

async def verify_rotate(
self,
wallet: BaseWallet,
attached: AttachDecorator,
invi_key: str = None,
) -> str:
"""Verify a signed DID rotate attachment and return did."""
signed_diddoc_bytes = attached.data.signed
if not signed_diddoc_bytes:
raise DIDXManagerError("DID rotate attachment is not signed.")
if not await attached.data.verify(wallet, invi_key):
raise DIDXManagerError(
"DID rotate attachment signature failed verification"
)

return signed_diddoc_bytes.decode()

async def manager_error_to_problem_report(
self,
e: DIDXManagerError,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def __init__(
*,
did: str = None,
did_doc_attach: Optional[AttachDecorator] = None,
did_rotate_attach: Optional[AttachDecorator] = None,
**kwargs,
):
"""Initialize DID exchange response object under RFC 23.
Expand All @@ -40,6 +41,7 @@ def __init__(
super().__init__(**kwargs)
self.did = did
self.did_doc_attach = did_doc_attach
self.did_rotate_attach = did_rotate_attach


class DIDXResponseSchema(AgentMessageSchema):
Expand All @@ -61,3 +63,9 @@ class Meta:
data_key="did_doc~attach",
metadata={"description": "As signed attachment, DID Doc associated with DID"},
)
did_rotate_attach = fields.Nested(
AttachDecoratorSchema,
required=False,
data_key="did_rotate~attach",
metadata={"description": "As signed attachment, DID signed by invitation key"},
)
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,13 @@ async def asyncSetUp(self):

did_doc_attach = AttachDecorator.data_base64(self.make_did_doc().serialize())
await did_doc_attach.data.sign(self.did_info.verkey, self.wallet)
did_rotate_attach = AttachDecorator.data_base64_string(self.test_verkey)
await did_rotate_attach.data.sign(self.did_info.verkey, self.wallet)

self.response = DIDXResponse(
did=TestConfig.test_did,
did_doc_attach=did_doc_attach,
did_rotate_attach=did_rotate_attach,
)

def test_init(self):
Expand Down Expand Up @@ -116,13 +119,17 @@ async def asyncSetUp(self):

did_doc_attach = AttachDecorator.data_base64(self.make_did_doc().serialize())
await did_doc_attach.data.sign(self.did_info.verkey, self.wallet)
did_rotate_attach = AttachDecorator.data_base64_string(self.test_verkey)
await did_rotate_attach.data.sign(self.did_info.verkey, self.wallet)

self.response = DIDXResponse(
did=TestConfig.test_did,
did_doc_attach=did_doc_attach,
did_rotate_attach=did_rotate_attach,
)

async def test_make_model(self):
data = self.response.serialize()
model_instance = DIDXResponse.deserialize(data)
assert isinstance(model_instance, DIDXResponse)
assert model_instance.did_rotate_attach
Loading
Loading