Skip to content

Commit

Permalink
tdw
Browse files Browse the repository at this point in the history
Signed-off-by: pstlouis <[email protected]>
  • Loading branch information
PatStLouis committed Sep 10, 2024
1 parent 8c505da commit ff34ded
Show file tree
Hide file tree
Showing 12 changed files with 243 additions and 85 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,11 @@ sequenceDiagram
- DID log support
- whois VP support
- AnonCreds objects support
- Status lists support
- Status lists support

```bash
In a web where trust is born anew,
Decentralized keys unlock our view.
We shape the code, and break the chain,
Trust in our hands will always reign.
```
4 changes: 2 additions & 2 deletions demo/manage
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ sudo docker compose --env-file .env.demo up --build
sudo docker compose --env-file .env.demo up --build -d
;;
restart)
sudo docker compose --env-file .env.demo up down -v
sudo docker compose --env-file .env.demo down -v
sudo docker compose --env-file .env.demo up --build -d
;;
stop)
sudo docker compose --env-file .env.demo up down -v
sudo docker compose --env-file .env.demo down -v
;;
*)
usage;;
Expand Down
5 changes: 3 additions & 2 deletions server/app/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from fastapi import FastAPI, APIRouter
from fastapi.responses import JSONResponse
from app.routers import identifiers
from app.routers import identifiers, resolvers
from config import settings

app = FastAPI(title=settings.PROJECT_TITLE, version=settings.PROJECT_VERSION)
Expand All @@ -12,7 +12,8 @@
async def server_status():
return JSONResponse(status_code=200, content={"status": "ok"})

api_router.include_router(identifiers.router, tags=["Identifiers"])
api_router.include_router(identifiers.router)
api_router.include_router(resolvers.router)



Expand Down
7 changes: 5 additions & 2 deletions server/app/models/did_document.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Union, List, Dict
from typing import Union, List, Dict, Any
from pydantic import BaseModel, Field
from .di_proof import DataIntegrityProof

Expand All @@ -18,7 +18,10 @@ class Service(BaseModel):

class DidDocument(BaseModel):
context: Union[str, List[str]] = Field(
["https://www.w3.org/ns/did/v1"], alias="@context"
[
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1"
], alias="@context"
)
id: str = Field(None)
controller: str = Field(None)
Expand Down
28 changes: 25 additions & 3 deletions server/app/models/web_requests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
from typing import Union, List, Dict
from typing import Union, List, Dict, Any
from pydantic import BaseModel, Field
from .did_document import DidDocument
from config import settings

class BaseRequest(BaseModel):
def model_dump(self, **kwargs) -> Dict[str, Any]:
return super().model_dump(by_alias=True, exclude_none=True, **kwargs)

class RegisterDID(BaseModel):
didDocument: DidDocument = Field()

class RequestDID(BaseRequest):
namespace: str = Field(example="example")
identifier: str = Field(example="issuer")


class RegisterDID(BaseRequest):
didDocument: DidDocument = Field(example={
"@context": [],
"id": f"{settings.DID_WEB_BASE}:example:issuer"
})


class RequestDIDUpgrade(BaseRequest):
id: str = Field(example=f"{settings.DID_WEB_BASE}:example:issuer")
updateKey: str = Field(example="z...")


class PublishLogEntry(BaseRequest):
logEntry: list = Field()
43 changes: 30 additions & 13 deletions server/app/plugins/askar.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from aries_askar import Store, error, Key
from aries_askar.bindings import LocalKeyHandle
from config import settings
from app.utilities import create_did_doc
from app.models.did_document import DidDocument
import hashlib
import uuid
from multiformats import multibase
Expand All @@ -21,9 +21,22 @@ def __init__(self):

async def provision(self, recreate=False):
await Store.provision(self.db, "raw", self.key, recreate=recreate)
endorser_did_doc = create_did_doc(
did=settings.DID_WEB_BASE, multikey=settings.ENDORSER_MULTIKEY
)
did = settings.DID_WEB_BASE
kid = f'{settings.DID_WEB_BASE}#key-01'
endorser_did_doc = DidDocument(
id=did,
verificationMethod=[
{
"id": kid,
"type": "Multikey",
"controller": did,
"publicKeyMultibase": settings.ENDORSER_MULTIKEY,
}
],
authentication=[kid],
assertionMethod=[kid],
service=[],
).dict(by_alias=True, exclude_none=True)
try:
await self.store("didDocument", settings.DID_WEB_BASE, endorser_did_doc)
except:
Expand Down Expand Up @@ -78,22 +91,26 @@ def __init__(self, multikey=None):
alg="ed25519", public=bytes(bytearray(multibase.decode(multikey))[2:])
)

def create_proof_config(self):
def create_proof_config(self, challenge=None):
created = str(datetime.now(timezone.utc).isoformat("T", "seconds"))
expires = str(
(datetime.now(timezone.utc) + timedelta(minutes=10)).isoformat(
"T", "seconds"
)
)
return {
proof_options = {
"type": self.type,
"cryptosuite": self.cryptosuite,
"proofPurpose": self.purpose,
"created": created,
"expires": expires,
"domain": settings.DOMAIN,
"challenge": self.create_challenge(created + expires),
"challenge": challenge,
}
if not challenge:
expires = str(
(datetime.now(timezone.utc) + timedelta(minutes=10)).isoformat(
"T", "seconds"
)
)
proof_options['expires'] = expires
proof_options['challenge'] = self.create_challenge(created + expires)

return proof_options

def create_challenge(self, value):
return str(uuid.uuid5(uuid.NAMESPACE_DNS, settings.SECRET_KEY + value))
Expand Down
26 changes: 16 additions & 10 deletions server/app/plugins/trust_did_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ def _define_parameters(self, update_key=None, next_key=None, ttl=100):
def _generate_entry_hash(self, log_entry):
# https://identity.foundation/trustdidweb/#generate-entry-hash
jcs = canonicaljson.encode_canonical_json(log_entry)
multihash = multihash.digest(jcs.encode(), 'sha2-256').hex()
encoded = multibase.encode(multihash, 'base58btc')[1:]
multihashed = multihash.digest(jcs, 'sha2-256')
encoded = multibase.encode(multihashed, 'base58btc')[1:]
return encoded

def _generate_scid(self, log_entry):
# https://identity.foundation/trustdidweb/#generate-scid
jcs = canonicaljson.encode_canonical_json(log_entry)
multihash = multihash.digest(jcs.encode(), 'sha2-256').hex()
encoded = multibase.encode(multihash, 'base58btc')[1:]
multihashed = multihash.digest(jcs, 'sha2-256')
encoded = multibase.encode(multihashed, 'base58btc')[1:]
return encoded

def _add_placeholder_scid(self, item):
Expand All @@ -52,11 +52,11 @@ def _web_to_tdw(self, did_doc):
for idx, item in enumerate(did_doc['verificationMethod']):
did_doc['verificationMethod'][idx] = self._add_placeholder_scid(did_doc['verificationMethod'][idx])

def _init_parameters(self):
def _init_parameters(self, update_key):
return {
"method": 'did:tdw:0.3',
"scid": r"{SCID}",
"updateKeys": [],
"updateKeys": [update_key],
"portable": False,
"prerotation": False,
"nextKeyHashes": [],
Expand All @@ -68,16 +68,22 @@ def _init_did_doc(self):
"@context": [],
"id": r"{SCID}",
}
def provision_log_entry(self, did_doc):
did_doc['id'] = did_doc['id'].replace('did:web:', r'did:tdw:{SCID}:')
return [

def provision_log_entry(self, did_doc, update_key):
did_doc = json.loads(json.dumps(did_doc).replace('did:web:', r'did:tdw:{SCID}:'))
preliminary_did_log_entry = [
r'{SCID}',
str(datetime.now(timezone.utc).isoformat("T", "seconds")),
self._init_parameters(),
self._init_parameters(update_key=update_key),
{
"value": did_doc
}
]
scid = self._generate_scid(preliminary_did_log_entry)
log_entry = json.loads(json.dumps(preliminary_did_log_entry).replace('{SCID}', scid))
entry_hash = self._generate_entry_hash(log_entry)
log_entry[0] = f'1-{entry_hash}'
return log_entry

def create(self, did_doc):
# https://identity.foundation/trustdidweb/#create-register
Expand Down
102 changes: 52 additions & 50 deletions server/app/routers/identifiers.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,70 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Response
from fastapi.responses import JSONResponse
from app.models.web_requests import RegisterDID
from app.models.web_requests import RequestDID, RegisterDID, RequestDIDUpgrade, PublishLogEntry
from config import settings
from app.plugins import AskarVerifier, AskarStorage, TrustDidWeb
from app.dependencies import (
identifier_available,
did_document_exists,
valid_did_registration,
)
from app.utilities import create_did_doc_template
# from app.dependencies import (
# did_document_exists,
# valid_did_registration,
# )
from app.utilities import location_available, to_did_web, valid_did_registration, did_document_exists, bootstrap_did_doc
import jsonlines
import json

router = APIRouter()
router = APIRouter(tags=["Identifiers"])


@router.get("/{namespace}/{identifier}", summary="Request DID configuration.")
async def get_did(
namespace: str, identifier: str, logs: bool = False, dependency=Depends(identifier_available)
):
did_doc = create_did_doc_template(namespace, identifier)
if logs:
return JSONResponse(
status_code=200,
content={
"logEntry": TrustDidWeb().provision_log_entry(did_doc),
"options": AskarVerifier().create_proof_config(),
},
)
@router.post("/did/request", summary="Request new identifier.")
async def request_did(request_body: RequestDID):
did = to_did_web(request_body.model_dump()['namespace'], request_body.model_dump()['identifier'])
await location_available(did)
return JSONResponse(
status_code=200,
content={
"document": did_doc,
"document": bootstrap_did_doc(did),
"options": AskarVerifier().create_proof_config(),
},
)


@router.post("/{namespace}/{identifier}", summary="Register DID.")
async def register_did(
request_body: RegisterDID,
namespace: str,
identifier: str,
did_document=Depends(valid_did_registration),
):
await AskarStorage().store("didDocument", f"{namespace}:{identifier}", did_document)
return JSONResponse(status_code=201, content={"didDocument": did_document})


@router.put("/{namespace}/{identifier}", summary="Update DID document.")
async def update_did(
namespace: str, identifier: str, dependency=Depends(did_document_exists)
):
raise HTTPException(status_code=501, detail="Not implemented.")
@router.post("/did/register", summary="Register identifier.")
async def upgrade_did(request_body: RegisterDID):
did_document = request_body.model_dump()['didDocument']
await location_available(did_document['id'])
await valid_did_registration(did_document)
await AskarStorage().store("didDocument", did_document['id'], did_document)
return JSONResponse(
status_code=201,
content={
"didDocument": did_document,
},
)


@router.delete("/{namespace}/{identifier}", summary="Archive DID.")
async def delete_did(
namespace: str, identifier: str, dependency=Depends(did_document_exists)
):
raise HTTPException(status_code=501, detail="Not implemented.")
@router.post("/did/upgrade", summary="Upgrade to Trust DID Web.")
async def request_did_upgrade(request_body: RequestDIDUpgrade):
await did_document_exists(request_body.model_dump()['id'])
did_document = await AskarStorage().fetch("didDocument", request_body.model_dump()['id'])
log_entry = TrustDidWeb().provision_log_entry(did_document, request_body.model_dump()['updateKey'])
return JSONResponse(
status_code=200,
content={
"logEntry": log_entry,
"proofOptions": AskarVerifier().create_proof_config(challenge=log_entry[0]),
},
)


@router.get("/{namespace}/{identifier}/did.json", summary="Get DID document.")
async def get_did(
namespace: str, identifier: str, dependency=Depends(did_document_exists)
):
did_doc = await AskarStorage().fetch("didDocument", f"{namespace}:{identifier}")
return JSONResponse(status_code=200, content=did_doc)
@router.post("/did/log", summary="Publish log entry.")
async def publish_log(request_body: PublishLogEntry, response: Response):
log_entry = request_body.model_dump()['logEntry']
did_tdw = log_entry[3]['value']['id']
did_web = 'did:web:'+':'.join(did_tdw.split(':')[3:])
await did_document_exists(did_web)
# await valid_log_entry(log_entry)
did_document = await AskarStorage().fetch("didDocument", did_web)
did_document['alsoKnownAs'] = log_entry[3]['value']['id']
did_document = await AskarStorage().update("didDocument", did_web, did_document)
logs = [json.dumps(log_entry)]
did_document = await AskarStorage().store("didLogs", did_web, logs)
return JSONResponse(status_code=201, content={'did': did_tdw})
35 changes: 35 additions & 0 deletions server/app/routers/resolvers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from fastapi import APIRouter, Depends, HTTPException, Response
from fastapi.responses import JSONResponse
from config import settings
from app.plugins import AskarVerifier, AskarStorage, TrustDidWeb
from app.utilities import to_did_web, did_document_exists
import jsonlines
import json

router = APIRouter(tags=["Resolvers"])

@router.get("/.well-known/did.json")
async def get_endorser_did():
did_document = await AskarStorage().fetch("didDocument", settings.DID_WEB_BASE)
return JSONResponse(status_code=200, content=did_document)

@router.get("/{namespace}/{identifier}/did.json")
async def get_did(
namespace: str, identifier: str
):
did = to_did_web(namespace, identifier)
await did_document_exists(did)
did_document = await AskarStorage().fetch("didDocument", did)
return JSONResponse(status_code=200, content=did_document)


@router.get("/{namespace}/{identifier}/did.jsonl")
async def get_did_logs(
namespace: str, identifier: str, response: Response
):
did = to_did_web(namespace, identifier)
await did_document_exists(did)
did_logs = await AskarStorage().fetch("didLogs", did)
did_logs = jsonlines.Reader(did_logs).read()
response.headers['Content-Type'] = 'application/octet-stream'
return did_logs
Loading

0 comments on commit ff34ded

Please sign in to comment.