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: add optional validation of input doc #4

Merged
merged 3 commits into from
Oct 20, 2023
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
15 changes: 15 additions & 0 deletions did_peer_4/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from base58 import b58decode, b58encode
from hashlib import sha256

from .valid import validate_input_document

# Regex patterns
BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
Expand Down Expand Up @@ -55,8 +56,11 @@ def _hash_encoded_doc(encoded_doc: str) -> str:

def encode(
document: Dict[str, Any],
validate: bool = True,
) -> str:
"""Encode an input document into a did:peer:4."""
if validate:
document = dict(validate_input_document(document))
encoded_doc = _encode_doc(document)
hashed = _hash_encoded_doc(encoded_doc)
return f"did:peer:4{hashed}:{encoded_doc}"
Expand Down Expand Up @@ -195,3 +199,14 @@ def resolve_short_from_doc(
raise ValueError("Document does not match DID")

return resolve_short(long)


__all__ = [
"encode",
"encode_short",
"decode",
"resolve",
"resolve_short",
"resolve_short_from_doc",
"validate_input_document",
]
76 changes: 76 additions & 0 deletions did_peer_4/valid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Validate input documents."""
from typing import Any, Mapping


def resources(document: Mapping[str, Any]):
"""Yield all resources in a document, skipping references."""
keys = (
"verificationMethod",
"authentication",
"assertionMethod",
"keyAgreement",
"capabilityDelegation",
"capabilityInvocation",
"service",
)
for key in keys:
if key in document:
if not isinstance(document[key], list):
raise ValueError(f"{key} must be a list")

for index, resource in enumerate(document[key]):
if isinstance(resource, dict):
yield key, index, resource


def validate_input_document(document: Mapping[str, Any]) -> Mapping[str, Any]:
"""Validate did:peer:4 input document.

This validation is deliberately superficial. It is intended to catch mistakes
in the input document that would cause the peer DID to be invalid. It is not
intended to validate the contents of the document, which is left to the caller
after resolution.

The following checks are performed:

- The document must be a Mapping.
- The document must not be empty.
- The document must not contain an id.
- If present, alsoKnownAs must be a list.
- verificationMethod, authentication, assertionMethod, keyAgreement,
capabilityDelegation, capabilityInvocation, and service must be lists, if
present.
- All resources (verification methods, embedded verification methods,
services) must have an id.
- All resource ids must be strings.
- All resource ids must be relative.
- All resources must have a type.
"""
if not isinstance(document, Mapping):
raise ValueError("document must be a Mapping")

if not document:
raise ValueError("document must not be empty")

if "id" in document:
raise ValueError("id must not be present in input document")

if "alsoKnownAs" in document:
if not isinstance(document["alsoKnownAs"], list):
raise ValueError("alsoKnownAs must be a list")

for key, index, resource in resources(document):
if "id" not in resource:
raise ValueError(f"{key}[{index}]: resource must have an id")

ident = resource["id"]
if not isinstance(ident, str):
raise ValueError(f"{key}[{index}]: resource id must be a string")

if not ident.startswith("#"):
raise ValueError(f"{key}[{index}]: resource id must be relative")

if "type" not in resource:
raise ValueError(f"{key}[{index}]: resource must have a type")

return document
121 changes: 120 additions & 1 deletion pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,14 @@ dev = [
"black>=23.7.0",
"ruff>=0.0.285",
"pre-commit>=3.3.3",
"pytest-cov>=4.1.0",
]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"@abstract"
]
precision = 2
skip_covered = true
show_missing = true
11 changes: 11 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Tests for did:peer:4."""
from pathlib import Path


def examples():
"""Load json from examples directory and return generator over examples."""
examples_dir = Path(__file__).parent / "examples"
yield from examples_dir.glob("*.json")


EXAMPLES = list(examples())
10 changes: 1 addition & 9 deletions tests/test_readme_examples.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import json
import pytest
from pathlib import Path

from did_peer_4 import encode, long_to_short, resolve, resolve_short


def examples():
"""Load json from examples directory and return generator over examples."""
examples_dir = Path(__file__).parent / "examples"
yield from examples_dir.glob("*.json")


EXAMPLES = list(examples())
from . import EXAMPLES


def print_example(
Expand Down
Loading