Skip to content

Add a policy module #102

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

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
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
46 changes: 44 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,47 @@ sequenceDiagram

An attested resource is a stored resource cryptographically bound to it's location on the web. See the anoncreds document for more information.

## Roadmap
- whois VP support
### Setting up policies

The server is equiped with a configurable policy module. Rule sets can be established to change the server behavior when validating some requests.

#### Known Witnesses Registry

A default known witness key to provision the server.
`KNOWN_WITNESS_KEY=z6MQ`

A list of known witnesses is used for validating witness policies.
`KNOWN_WITNESS_REGISTRY=https://known-witnesses.example.com`

#### Attested Resource Endorsement

This will require a known witness proof on any attested resource uploaded or updated. It's up to the witness service to determine which resources to endorse from the controller.
`WEBVH_ENDORSEMENT=true`

#### WebVH Parameters

Require some webvh paramters to be enabled.

##### WebVH Method Version

`WEBVH_VERSION=1.0`

##### Witness

Require a signature from at least 1 known witness.
`WEBVH_WITNESS=true`

##### Watcher

Require a specific watcher to monitor the server.
`WEBVH_WATCHER=https://watcher.example.com`

##### Prerotation

Require prerotation
`WEBVH_PREROTATION=true`

##### Portability

Require portability
`WEBVH_PORTABILITY=true`
32 changes: 31 additions & 1 deletion server/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
DOMAIN=''
SECRET_KEY=''
ENDORSER_MULTIKEY=''
ENDORSER_MULTIKEY=''


# POLICIES

# Provide a known witness key
KNOWN_WITNESS_KEY=null

# Registry to used for known witnesses
KNOWN_WITNESS_REGISTRY=https://registry.example.com

# Require a known witness proof on attested resources
WEBVH_ENDORSEMENT=true

# Enforce a specific webvh version
WEBVH_VERSION=1.0

# Require at least 1 known witness signature
WEBVH_WITNESS=true

# URL for mandatory watcher service
WEBVH_WATCHER=https://watcher.example.com

# Boolean to enforce key rotation
WEBVH_PREROTATION=true

# Boolean to enforce portability
WEBVH_PORTABILITY=true

# Positive integer value representing the time range in hours acceptable for timestamps
WEBVH_VALIDITY=1
12 changes: 2 additions & 10 deletions server/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from app.routers import admin, identifiers, resources
from app.contexts import AttestedResourceCtx
from app.routers import policies, identifiers, resources
import json
import logging
from config import settings
Expand Down Expand Up @@ -35,14 +34,7 @@ async def server_status():
return JSONResponse(status_code=200, content={"status": "ok"})


@api_router.get("/attested-resource/v1", tags=["Context"], include_in_schema=False)
async def get_attested_resource_ctx():
"""Attested Resource Context."""
ctx = json.dumps(AttestedResourceCtx, indent=2)
return Response(ctx, media_type="application/ld+json")


api_router.include_router(admin.router)
api_router.include_router(policies.router)
api_router.include_router(resources.router)
api_router.include_router(identifiers.router)

Expand Down
3 changes: 0 additions & 3 deletions server/app/contexts/__init__.py

This file was deleted.

31 changes: 0 additions & 31 deletions server/app/contexts/attested_resource.py

This file was deleted.

2 changes: 1 addition & 1 deletion server/app/models/did_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class Witness(BaseModel):
threshold: int = Field()
witnesses: List[Witness] = Field()

method: str = Field(None, example=f"did:webvh:{settings.WEBVH_VERSION}")
method: str = Field(None)
scid: str = Field(None)
portable: bool = Field(None)
updateKeys: List[str] = Field(None)
Expand Down
45 changes: 45 additions & 0 deletions server/app/models/policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""WebVH Server Policy."""

from typing import Any, Dict, Union
from pydantic import BaseModel, Field


class BaseModel(BaseModel):
"""Base model for all models in the application."""

def model_dump(self, **kwargs) -> Dict[str, Any]:
"""Dump the model to a dictionary."""
return super().model_dump(by_alias=True, exclude_none=True, **kwargs)


class ActivePolicy(BaseModel):
"""Model for server policies."""

version: str = Field(None)
witness: bool = Field(True)
watcher: Union[str, None] = Field(None)
portability: bool = Field(True)
prerotation: bool = Field(True)
endorsement: bool = Field(True)
validity: int = Field(0)
witness_registry_url: Union[str, None] = Field(None)


class KnownWitnessRegistry(BaseModel):
"""Model for witness registry."""

class RegistryMetadata(BaseModel):
"""Model for witness registry metadata."""

created: str = Field()
updates: str = Field()

class RegistryEntry(BaseModel):
"""Model for witness registry entry."""

url: str = Field(None)
name: str = Field(None)
location: str = Field(None)

meta: RegistryMetadata = Field()
registry: Dict[str, RegistryEntry] = Field()
2 changes: 1 addition & 1 deletion server/app/models/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ class AttestedResource(BaseModel):
context: List[str] = Field(
alias="@context",
default=[
settings.ATTESTED_RESOURCE_CTX,
"https://w3id.org/security/data-integrity/v2",
f"https://{settings.DOMAIN}/attested-resource/v1",
],
)
type: List[str] = Field(default=["AttestedResource"])
Expand Down
15 changes: 11 additions & 4 deletions server/app/models/web_schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Pydantic models for the web schemas."""

from typing import Any, Dict, List
from typing import Any, Dict, List, Union

from pydantic import BaseModel, Field
from .did_document import SecuredDidDocument
Expand Down Expand Up @@ -35,14 +35,21 @@ class NewLogEntry(BaseModel):
"""NewLogEntry model."""

logEntry: LogEntry = Field()
witnessSignature: WitnessSignature = Field(None)
witnessSignature: Union[WitnessSignature, None] = Field(None)


class UpdateLogEntry(BaseModel):
"""UpdateLogEntry model."""

logEntry: LogEntry = Field()
witnessProof: List[DataIntegrityProof] = Field(None)

class WitnessProof(BaseModel):
"""WitnessProof model."""

versionId: str = Field()
proof: List[DataIntegrityProof] = Field()

witnessProof: WitnessProof = Field(None)


class DeactivateLogEntry(BaseModel):
Expand Down Expand Up @@ -84,7 +91,7 @@ class ResourceUpload(BaseModel):
"""ResourceUpload model."""

attestedResource: AttestedResource = Field()
options: ResourceOptions = Field()
options: ResourceOptions = Field(None)


class WhoisUpdate(BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions server/app/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .askar import AskarStorage, AskarVerifier
from .didwebvh import DidWebVH
from .didwebvh import DidWebVH, PolicyError

__all__ = ["AskarVerifier", "AskarStorage", "DidWebVH"]
__all__ = ["AskarVerifier", "AskarStorage", "DidWebVH", "PolicyError"]
52 changes: 19 additions & 33 deletions server/app/plugins/askar.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from fastapi import HTTPException
from multiformats import multibase
from app.utilities import timestamp
from app.models.policy import (
ActivePolicy,
) # , KnownWitnessRegistry, RegistryMetadata, RegistryEntry

from config import settings

Expand All @@ -23,24 +26,35 @@ class AskarStorage:
def __init__(self):
"""Initialize the Askar storage plugin."""
self.db = settings.ASKAR_DB
self.key = Store.generate_raw_key(hashlib.md5(settings.STORAGE_KEY.encode()).hexdigest())

async def provision(self, recreate=False):
"""Provision the Askar storage."""
await Store.provision(self.db, "raw", self.key, recreate=recreate)
await Store.provision(self.db, "none", recreate=recreate)
if not await self.fetch("registry", "knownWitnesses"):
witness_registry = {
"meta": {"created": timestamp(), "updated": timestamp()},
"registry": {},
}
if settings.DEFAULT_WITNESS_KEY:
witness_did = f"did:key:{settings.DEFAULT_WITNESS_KEY}"
if settings.KNOWN_WITNESS_KEY:
witness_did = f"did:key:{settings.KNOWN_WITNESS_KEY}"
witness_registry["registry"][witness_did] = {"name": "Default Server Witness"}
await self.store("registry", "knownWitnesses", witness_registry)

if not await self.fetch("policy", "active"):
policy = ActivePolicy(
version=settings.WEBVH_VERSION,
witness=settings.WEBVH_WITNESS,
watcher=settings.WEBVH_WATCHER,
portability=settings.WEBVH_PORTABILITY,
prerotation=settings.WEBVH_PREROTATION,
endorsement=settings.WEBVH_ENDORSEMENT,
witness_registry_url=settings.KNOWN_WITNESS_REGISTRY,
).model_dump()
await self.store("policy", "active", policy)

async def open(self):
"""Open the Askar storage."""
return await Store.open(self.db, "raw", self.key)
return await Store.open(self.db, "none")

async def fetch(self, category, data_key):
"""Fetch data from the store."""
Expand Down Expand Up @@ -104,34 +118,6 @@ def __init__(self):
self.cryptosuite = "eddsa-jcs-2022"
self.purpose = "assertionMethod"

def create_proof_config(self, did):
"""Create a proof configuration."""
expires = timestamp(minutes_delta=settings.REGISTRATION_PROOF_TTL)
return {
"type": self.type,
"cryptosuite": self.cryptosuite,
"proofPurpose": self.purpose,
"expires": expires,
"domain": settings.DOMAIN,
"challenge": self.create_challenge(did + expires),
}

def create_challenge(self, value):
"""Create a challenge."""
return str(uuid.uuid5(uuid.NAMESPACE_DNS, settings.SECRET_KEY + value))

def validate_challenge(self, proof, did):
"""Validate the challenge."""
try:
if proof.get("domain"):
assert proof["domain"] == settings.DOMAIN, "Domain mismatch."
if proof.get("challenge"):
assert proof["challenge"] == self.create_challenge(did + proof["expires"]), (
"Challenge mismatch."
)
except AssertionError as msg:
raise HTTPException(status_code=400, detail=str(msg))

def validate_proof(self, proof):
"""Validate the proof."""
try:
Expand Down
Loading
Loading