Skip to content

Commit

Permalink
feat: emit did:peer:2 DID in DID Exchange
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Bluhm <[email protected]>
  • Loading branch information
dbluhm committed Oct 31, 2023
1 parent 056f724 commit 8dec969
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 27 deletions.
10 changes: 10 additions & 0 deletions aries_cloudagent/config/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,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 @@ -1297,6 +1303,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
69 changes: 67 additions & 2 deletions aries_cloudagent/connections/base_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,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 Down Expand Up @@ -44,8 +46,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 @@ -77,6 +79,69 @@ def __init__(self, profile: Profile):
logger_name=__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": ["#keys-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
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 @@ -591,6 +591,40 @@ def content(self) -> Union[Mapping, Tuple[Sequence[str], str]]:
else:
return None

@classmethod
def data_base65_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
96 changes: 72 additions & 24 deletions aries_cloudagent/protocols/didexchange/v1_0/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,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 @@ -307,17 +321,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 @@ -598,6 +602,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 @@ -613,6 +627,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 @@ -628,20 +646,16 @@ 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
if conn_rec.invitation_msg_id is None:
# Rotation needed
attach = AttachDecorator.data_base65_string(did)
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_rotate_attach=attach)
else:
did_doc = await self.create_did_document(
my_info,
Expand All @@ -652,8 +666,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 @@ -770,6 +784,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 @@ -942,6 +973,23 @@ async def verify_diddoc(

return DIDDoc.deserialize(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 get_resolved_did_document(self, qualified_did: str) -> ResolvedDocument:
"""Return resolved DID document."""
resolver = self._profile.inject(DIDResolver)
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"},
)
9 changes: 8 additions & 1 deletion aries_cloudagent/wallet/did_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Dict, List, Mapping, Optional

from .error import BaseError
from .key_type import BLS12381G2, ED25519, KeyType
from .key_type import BLS12381G2, ED25519, X25519, KeyType


class HolderDefinedDid(Enum):
Expand Down Expand Up @@ -70,6 +70,12 @@ def holder_defined_did(self) -> HolderDefinedDid:
key_types=[ED25519, BLS12381G2],
rotation=False,
)
PEER2 = DIDMethod(
name="did:peer:2",
key_types=[ED25519, X25519],
rotation=False,
holder_defined_did=HolderDefinedDid.NO,
)


class DIDMethods:
Expand All @@ -80,6 +86,7 @@ def __init__(self) -> None:
self._registry: Dict[str, DIDMethod] = {
SOV.method_name: SOV,
KEY.method_name: KEY,
PEER2.method_name: PEER2,
}

def registered(self, method: str) -> bool:
Expand Down

0 comments on commit 8dec969

Please sign in to comment.