Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

### Versions prior to 0.3.0 (hyperledger-labs/did-webs-resolver history)

### 0.3.4 Upgrade to KERIpy 1.2.12 and test fixes

Just a few test fixes to get test coverage back to 100%, a quoting fix on re-encoding invalid did:webs DIDs, and upgrade to KERIpy 1.2.12, the latest release.

### 0.3.2 Delegation and alsoKnownAs features

- Added support for delegator blocks in `service` section.
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
build-dynamic-service publish-dynamic-service tag-dynamic-service-latest \
build-did-webs-resolver-service publish-did-webs-resolver-service tag-did-webs-resolver-latest \
run-agent build-all publish-latest warn tag fmt check tag-latest-all
VERSION=0.3.2 # also change in pyproject.toml and src/dws/__init__.py
VERSION=0.3.4 # also change in pyproject.toml and src/dws/__init__.py

RED="\033[0;31m"
NO_COLOUR="\033[0m"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A DID resolver for did:webs and did:keri DIDs also compatible with the Universal Resolver.

A demonstration of a `did:webs` service and resolver. Implements the
`did:webs` [specification](https://trustoverip.github.io/tswg-did-method-webs-specification/).
`did:webs` [specification](https://trustoverip.github.io/kswg-did-method-webs-specification/).

[![CI](https://github.com/GLEIF-IT/did-webs-resolver/actions/workflows/ci.yml/badge.svg)](https://github.com/GLEIF-IT/did-webs-resolver/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/GLEIF-IT/did-webs-resolver/branch/main/graph/badge.svg?token=sUADtbanWC)](https://codecov.io/gh/GLEIF-IT/did-webs-resolver)
Expand Down
17 changes: 7 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
[project]
name = "did-webs-resolver"
version = "0.3.2" # also change in src/dws/__init__.py and Makefile
version = "0.3.4" # also change in src/dws/__init__.py and Makefile
description = "did:webs DID Method Resolver"
readme = "README.md"
requires-python = "==3.12.6"
requires-python = ">=3.12.6,<3.14"
license = {text = "Apache Software License 2.0"}
authors = [
{name = "Philip S. Feairheller", email = "pfeairheller@gmail.com"},
{name = "Kent O. P. Bull", email = "kent@kentbull.com"}
{name = "Kent Bull", email = "kent@kentbull.com"}
]
keywords = ["did:webs", "did", "resolver", "webs", "keri", "acdc", "cesr", "vlei"]

dependencies = [
"keri==1.2.7",
"keri==1.2.12",
"multicommand>=1.0.0",
"falcon>=4.0.2",
"falcon>=3.1.3",
"requests>=2.32.5",
"viking>=0.1.1",
"vgate>=0.1.5",
]

[build-system]
Expand All @@ -37,11 +37,8 @@ dws = "dws.app.cli.dws:main"

[dependency-groups]
dev = [
"coverage>=7.8.0",
"coverage>=7.13.4",
"mockito>=1.5.4",
"pytest>=8.3.5",
"pytest-cov>=6.1.1",
]

[tool.uv.sources]
viking = { git = "https://github.com/GLEIF-IT/viking.git", rev = "350745c33322a939ce307a87a1205b0ebaf4e491" }
2 changes: 1 addition & 1 deletion src/dws/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '0.3.2' # also change in pyproject.toml and Makefile
__version__ = '0.3.4' # also change in pyproject.toml and Makefile

# Logging config
import logging
Expand Down
37 changes: 20 additions & 17 deletions src/dws/core/artifacting.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import json
import os

import viking
import vgate
from hio.core import http
from keri import kering
from keri.app import habbing, signing
from keri.app.habbing import Habery
from keri.core import serdering
from keri.app.habbing import Hab, Habery
from keri.core.serdering import SerderACDC
from keri.vdr import credentialing, viring
from keri.vdr.credentialing import Regery
from keri.vdr.viring import Reger

from dws import UnknownAID, log_name, ogler
from dws.core import didding, ends, resolving, webbing
from dws.core.didding import DES_ALIASES_SCHEMA

logger = ogler.getLogger(log_name)

Expand Down Expand Up @@ -61,7 +63,7 @@ def gen_tel_cesr(reger: viring.Reger, evt_pre: str) -> bytearray:
return msgs


def gen_acdc_cesr(hab: habbing.Hab, reger: credentialing.Reger, creder: serdering.SerderACDC) -> bytearray:
def gen_acdc_cesr(hab: Hab, reger: Reger, creder: SerderACDC) -> bytearray:
"""
Add the CESR stream of the self attestation ACDCs for the given AID including signatures
and their anchors to their source KELs.
Expand All @@ -72,9 +74,7 @@ def gen_acdc_cesr(hab: habbing.Hab, reger: credentialing.Reger, creder: serderin
return arr


def gen_des_aliases_cesr(
hab: habbing.Hab, reger: credentialing.Reger, aid: str, schema: str = didding.DES_ALIASES_SCHEMA
) -> bytearray:
def gen_des_aliases_cesr(hab: Hab, reger: Reger, aid: str, schema: str = DES_ALIASES_SCHEMA) -> bytearray:
"""
Select a specific ACDC from the local registry (Regery), if it exists, to generate the
CESR stream
Expand All @@ -90,15 +90,20 @@ def gen_des_aliases_cesr(
# self-attested, there is no issuee, and schema is designated aliases
local_creds = get_self_issued_acdcs(aid, reger, schema)

msgs = bytearray()
cesr_bytes = bytearray()
for cred in local_creds:
creder, *_ = reger.cloneCred(said=cred.qb64)
if creder.regi is not None:
# TODO check if this works if we only get the regi CESR stream once
msgs.extend(gen_tel_cesr(reger, creder.regi))
msgs.extend(gen_tel_cesr(reger, creder.said))
msgs.extend(gen_acdc_cesr(hab, reger, creder))
return msgs
cesr_bytes.extend(add_cred_cesr_bytes(creder, hab, reger))
return cesr_bytes


def add_cred_cesr_bytes(creder: SerderACDC, hab: Hab, reger: Reger) -> bytearray:
"""Add one ACDC credential bytes to the CESR stream"""
creder_bytes = bytearray()
creder_bytes.extend(gen_tel_cesr(reger, creder.regi))
creder_bytes.extend(gen_tel_cesr(reger, creder.said))
creder_bytes.extend(gen_acdc_cesr(hab, reger, creder))
return creder_bytes


def get_witness_loc_scheme_bytes(hab: habbing.Hab, wit_prefixes: list[str], scheme: str = '') -> bytearray:
Expand Down Expand Up @@ -149,8 +154,6 @@ def gen_loc_schemes_cesr(hab: habbing.Hab, aid: str, role: str = None, scheme=''
Generates a CESR stream of all location scheme record reply 'rpy' messages for a given AID based on
the witness location scheme and endpoint role records in the local Hab's database.

TODO handle agent and mailbox roles to get their location schemes and add them to the msgs.

Returns:
bytearray: CESR stream of location scheme and endpoint role records for the given AID and role.

Expand Down Expand Up @@ -234,7 +237,7 @@ def dyn_artifact_svr_doers(
doers = []
app = resolving.falcon_app()
webbing.load_endpoints(app, hby=hby, rgy=rgy, did_path=did_path, meta=meta)
voodoers = viking.setup(hby=hby, alias=alias)
voodoers = vgate.setup(hby=hby, alias=alias)
server = resolving.tls_falcon_server(app, http_port, keypath, certpath, cafilepath)
http_server_doer = http.ServerDoer(server=server)
doers.extend([http_server_doer])
Expand Down
32 changes: 25 additions & 7 deletions src/dws/core/didding.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@
flags=re.IGNORECASE,
)

# Detects the malformed shape:
# did:webs:<domain>:<port>[?<query>]
# where <port> is unencoded (":" not "%3A") and the required AID segment is absent.
NO_AID_UNENCODED_PORT_RE = re.compile(
pattern=r'\Adid:web(s)?:'
r'(?P<domain>[^%:]+)'
r':(?P<port>\d+)'
r'(?P<query>\?.*)?\Z',
flags=re.IGNORECASE,
)

DID_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
DID_TIME_PATTERN = re.compile(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z')

Expand Down Expand Up @@ -125,6 +136,14 @@ def parse_query_string(query: str) -> dict:


def re_encode_invalid_did_webs(did: str):
"""
Normalizes did:webs strings that arrive with an unencoded domain/port separator (:<port>) by
re-encoding it as %3A, while preserving path, AID, and query. This compensates for URL path
quote/unquote handling in the Falcon/HIO WSGI request path.
"""
if NO_AID_UNENCODED_PORT_RE.match(did):
raise ValueError(f'{did} is missing an AID')

match = DID_WEBS_UNENCODED_PORT_RE.match(did)
if match is None:
match = DID_WEBS_RE.match(did)
Expand All @@ -134,19 +153,18 @@ def re_encode_invalid_did_webs(did: str):

domain, port, path, aid, query = match.group('domain', 'port', 'path', 'aid', 'query')

if aid:
try:
_ = coring.Prefixer(qb64=aid)
except Exception as e:
raise ValueError(f'{aid} is an invalid AID')
try:
_ = coring.Prefixer(qb64=aid)
except Exception as e:
raise ValueError(f'{aid} is an invalid AID')

encoded = f'did:webs:{domain}'
if port:
encoded += f'%3A{port}'
if path:
encoded += f':{path}'
if aid:
encoded += f':{aid}'
# always add the AID
encoded += f':{aid}'
if query:
encoded += f'{query}'
return encoded
Expand Down
2 changes: 1 addition & 1 deletion src/dws/core/habs.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def get_habery_and_doer(
name: str | None, base: str | None, bran: str | None, cf: configing.Configer = None, temp: bool = False
) -> (habbing.Habery, habbing.HaberyDoer):
"""Get the Habery and its Doer respecting any existing AEID."""
aeid = get_auth_encryption_aid(name, base)
aeid = get_auth_encryption_aid(name, base, temp)
if aeid is None:
hby = habbing.Habery(name=name, base=base, bran=bran, cf=cf, temp=temp)
else:
Expand Down
17 changes: 8 additions & 9 deletions src/dws/core/resolving.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,7 @@ def error_resolution_response(error_message: str, differences: list) -> dict:
"""
resolution = dict() # Copy the actual DID document to modify it
resolution[didding.DD_FIELD] = None
if didding.DID_RES_META_FIELD not in resolution:
resolution[didding.DID_RES_META_FIELD] = dict()
resolution[didding.DID_RES_META_FIELD] = dict()
resolution[didding.DID_RES_META_FIELD]['error'] = 'notVerified'
resolution[didding.DID_RES_META_FIELD]['errorMessage'] = error_message
resolution[didding.DID_RES_META_FIELD]['differences'] = differences
Expand Down Expand Up @@ -445,12 +444,11 @@ def get_serve_dir(static_files_dir: str | None, did_doc_dir: str):

def serve_artifacts(app: falcon.App, hby: habbing.Habery, static_files_dir: str | None = None, did_path: str = ''):
"""Configures a Falcon HTTP server with a static file server for did.json and keri.cesr files based on a local directory."""
if static_files_dir is not None:
did_doc_dir = get_serve_dir(static_files_dir, hby.cf.get().get('did.doc.dir', ''))
route = '' if did_path is None else f'/{did_path}'
logger.info(f'Serving static files from {did_doc_dir} at route {route}')
# Host did:webs artifacts only if static path specified
app.add_static_route(route, did_doc_dir)
did_doc_dir = get_serve_dir(static_files_dir, hby.cf.get().get('did.doc.dir', ''))
route = '' if did_path is None else f'/{did_path}'
logger.info(f'Serving static files from {did_doc_dir} at route {route}')
# Host did:webs artifacts only if static path specified
app.add_static_route(route, did_doc_dir)


def load_ends(
Expand All @@ -462,7 +460,8 @@ def load_ends(
did_path: str = '',
):
"""Set up Falcon HTTP server endpoints for resolving DIDs and hosting static files"""
serve_artifacts(app, hby, static_files_dir, did_path)
if static_files_dir is not None:
serve_artifacts(app, hby, static_files_dir, did_path)
resolve_end = UniversalResolverResource(hby=hby, rgy=rgy, oobiery=oobiery, load_url=requesting.load_url_with_requests)
app.add_route('/1.0/identifiers/{did}', resolve_end)
app.add_route('/health', ends.HealthEnd())
Expand Down
36 changes: 36 additions & 0 deletions tests/dws/core/ends/test_keri_cesr_resource_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from mockito import mock, when

from dws.core import didding
from dws.core.artifacting import gen_loc_schemes_cesr
from dws.core.ends.keri_cesr_resource_end import KeriCesrResourceEnd


Expand Down Expand Up @@ -149,6 +150,41 @@ def test_keri_cesr_resource_end_on_get_single_sig():
)


def test_gen_loc_schemes_cesr_with_mailbox_role_skips_agent():
hab = mock()
hab.kever = mock()
kever = mock(eventing.Kever)
hab.kevers = {'test_aid': kever}
# hab.kever.wits = ['BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha']

hab.db = mock(basing.Baser)
hab.db.ends = mock(koming.Komer)

when(hab.db.ends).getItemIter(keys=('test_aid', kering.Roles.mailbox)).thenReturn(
[((None, 'mailbox', 'BDilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha'), None)]
)

when(hab).loadLocScheme(eid='BDilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha').thenReturn(
bytearray(
b'{"r":"/loc/scheme","a":{"eid":"BDilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","scheme":"tcp","url":"tcp://127.0.0.1:5632/"}}'
)
)

when(hab).loadEndRole(
cid='test_aid', eid='BDilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', role=kering.Roles.mailbox
).thenReturn(
bytearray(
b'{"r":"/end/role/add","a":{"eid":"BDilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","role":"mailbox","cid":"test_aid"}}'
)
)
loc_scheme_cesr = gen_loc_schemes_cesr(hab, 'test_aid', role=kering.Roles.mailbox)
assert (
loc_scheme_cesr
== b'{"r":"/loc/scheme","a":{"eid":"BDilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","scheme":"tcp","url":"tcp://127.0.0.1:5632/"}}'
b'{"r":"/end/role/add","a":{"eid":"BDilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","role":"mailbox","cid":"test_aid"}}'
)


def test_keri_cesr_resource_end_on_get_bad_path():
req = mock(falcon.Request)
rep = mock(falcon.Response)
Expand Down
6 changes: 4 additions & 2 deletions tests/dws/core/test_clienting.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_load_url_with_hio_invalid_url_throws():


def test_create_http_client_with_body_encodes():
url = 'http://example.com/api'
url = 'http://192.0.2.1/api'
body = json.dumps({'key': 'value'})
headers = {'Content-Type': 'application/json'}
client, client_doer = requesting.create_http_client('POST', url, body=body, headers=headers)
Expand All @@ -45,7 +45,9 @@ def test_create_http_client_with_body_encodes():


def test_load_url_with_hio_timeout_causes_hio_error():
bad_url = 'https://example.com/oobi/EN6Oh5XSD5_q2Hgu-aqpdfbVepdpYpFlgz6zvJL5b_r5/'
# Use RFC 5737 TEST-NET address (192.0.2.0/24) - not routed, connection hangs.
# example.com responds quickly (404), causing intermittent test failure.
bad_url = 'http://192.0.2.1/oobi/EN6Oh5XSD5_q2Hgu-aqpdfbVepdpYpFlgz6zvJL5b_r5/'
with pytest.raises(ArtifactResolveError) as exc_info:
requesting.load_url_with_hio(bad_url, timeout=0.1)
assert 'Failed to load URL' in str(exc_info.value), 'Should raise HioError on timeout'
Expand Down
42 changes: 42 additions & 0 deletions tests/dws/core/test_didding.py
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,48 @@ def test_re_encode_valid_did_webs_did_returns_original_did():
assert result == valid_did, f'Expected {valid_did}, but got {result}'


def test_re_encode_invalid_no_aid_did_webs_throws():
no_aid = f'did:webs:127.0.0.1:7676'
with pytest.raises(ValueError) as excinfo:
didding.re_encode_invalid_did_webs(no_aid)
assert str(excinfo.value) == f'{no_aid} is missing an AID'


@pytest.mark.parametrize(
'invalid_did,expected_did',
[
# no port, no path, no query
(
'did:webs:example.com:EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4',
'did:webs:example.com:EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4',
),
# port only
(
'did:webs:example.com:8443:EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4',
'did:webs:example.com%3A8443:EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4',
),
# path only
(
'did:webs:example.com:my:path:EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4',
'did:webs:example.com:my:path:EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4',
),
# query only
(
'did:webs:example.com:EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4?meta=true',
'did:webs:example.com:EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4?meta=true',
),
# port + path + query
(
'did:webs:example.com:8443:my:path:EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4?meta=true',
'did:webs:example.com%3A8443:my:path:EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4?meta=true',
),
],
)
def test_re_encode_invalid_did_webs_optional_parts(invalid_did, expected_did):
result = didding.re_encode_invalid_did_webs(invalid_did)
assert result == expected_did, f'Expected {expected_did}, but got {result}'


def test_re_encode_invalid_did_non_webs_raises():
invalid_did = 'did:example:123'
with pytest.raises(ValueError) as excinfo:
Expand Down
Loading
Loading