From 9608add9dd66d08536983b91c2931b60ebfed79f Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Fri, 10 Nov 2023 06:45:12 +0100 Subject: [PATCH] First iteration of the ssi routines --- .gitignore | 1 + README.md | 73 ++++++++++++++++++ idhub_ssikit/__init__.py | 58 ++++++++++++++ .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 3241 bytes main.py | 39 +++++----- ssikit_trustchain/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 188 -> 0 bytes vc_templates/academic.jsonld.j2 | 2 +- vc_templates/member-credential.json | 32 ++++++++ vc_templates/wikipedia.jsonld.j2 | 23 ++++++ 10 files changed, 207 insertions(+), 21 deletions(-) create mode 100644 .gitignore create mode 100644 idhub_ssikit/__init__.py create mode 100644 idhub_ssikit/__pycache__/__init__.cpython-311.pyc delete mode 100644 ssikit_trustchain/__init__.py delete mode 100644 ssikit_trustchain/__pycache__/__init__.cpython-311.pyc create mode 100644 vc_templates/member-credential.json create mode 100644 vc_templates/wikipedia.jsonld.j2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/README.md b/README.md index e69de29..8443717 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,73 @@ +# Helper routines to manage DIDs/VC/VPs + +This module is a wrapper around the functions exported by SpruceID's `DIDKit` framework. + +## DID generation and storage + +For now DIDs are of the kind `did:key`, with planned support for `did:web` in the near future. + +Creation of a DID involves two steps: +* Generate a unique DID controller key +* Derive a `did:key` type from the key + +Both must be stored in the IdHub database and linked to a `User` for later retrieval. + +```python +# Use case: generate and link a new DID for an existing user +user = request.user # ... + +controller_key = idhub_ssikit.generate_did_controller_key() +did_string = idhub_ssikit.keydid_from_controller_key(controller_key) + + +did = idhub.models.DID( + did = did_string, + user = user +) +did_controller_key = idhub.models.DIDControllerKey( + key_material = controller_key, + owner_did = did +) + +did.save() +did_controller_key.save() +``` + +## Verifiable Credential issuance + +Verifiable Credential templates are stored as Jinja2 (TBD) templates in `/schemas` folder. Please examine each template to see what data must be passed to it in order to render. + +The data passed to the template must at a minimum include: +* issuer_did +* subject_did +* vc_id + +For example, in order to render `/schemas/member-credential.json`: + +```python +from jinja2 import Environment, FileSystemLoader, select_autoescape +import idhub_ssikit + +env = Environment( + loader=FileSystemLoader("vc_templates"), + autoescape=select_autoescape() +) +unsigned_vc_template = env.get_template("member-credential.json") + +issuer_user = request.user +issuer_did = user.dids[0] # TODO: Django ORM pseudocode +issuer_did_controller_key = did.keys[0] # TODO: Django ORM pseudocode + +data = { + "vc_id": "http://pangea.org/credentials/3731", + "issuer_did": issuer_did, + "subject_did": "did:web:[...]", + "issuance_date": "2020-08-19T21:41:50Z", + "subject_is_member_of": "Pangea" +} +signed_credential = idhub_ssikit.render_and_sign_credential( + unsigned_vc_template, + issuer_did_controller_key, + data +) +``` \ No newline at end of file diff --git a/idhub_ssikit/__init__.py b/idhub_ssikit/__init__.py new file mode 100644 index 0000000..f8e03b6 --- /dev/null +++ b/idhub_ssikit/__init__.py @@ -0,0 +1,58 @@ +import asyncio +import datetime +import didkit +import json +import jinja2 + + +def generate_did_controller_key(): + return didkit.generate_ed25519_key() + + +def keydid_from_key(key): + return didkit.key_to_did("key", key) + + +def generate_generic_vc_id(): + # TODO agree on a system for Verifiable Credential IDs + return "https://pangea.org/credentials/42" + + +def render_and_sign_credential(vc_template: jinja2.Template, jwk_issuer, vc_data: dict[str, str]): + """ + Populates a VC template with data for issuance, and signs the result with the provided key. + + The `vc_data` parameter must at a minimum include: + * issuer_did + * subject_did + * vc_id + and must include whatever other fields are relevant for the vc_template to be instantiated. + + The following field(s) will be auto-generated if not passed in `vc_data`: + * issuance_date (to `datetime.datetime.now()`) + """ + async def inner(): + unsigned_vc = vc_template.render(vc_data) + signed_vc = await didkit.issue_credential( + unsigned_vc, + '{"proofFormat": "ldp"}', + jwk_issuer + ) + return signed_vc + + if vc_data.get("issuance_date") is None: + vc_data["issuance_date"] = datetime.datetime.now().replace(microsecond=0).isoformat() + + return asyncio.run(inner()) + + +def verify_credential(vc, proof_options): + """ + Returns a (bool, str) tuple indicating whether the credential is valid. + If the boolean is true, the credential is valid and the second argument can be ignored. + If it is false, the VC is invalid and the second argument contains a JSON object with further information. + """ + async def inner(): + return didkit.verify_credential(vc, proof_options) + + return asyncio.run(inner()) diff --git a/idhub_ssikit/__pycache__/__init__.cpython-311.pyc b/idhub_ssikit/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04f337611627aa851287f44a51c4d6a8805a65f3 GIT binary patch literal 3241 zcmai0-ESMm5#PI`NQ#sq+pXnT3F?h5Xn2h)LPml*jFAEkkRmA}J9g>-a)ZHDW7^}A3g4cb*2-AGBn7*||O^x+MUA>J( zL%nm0Ii}wsi(||HnrAtn1vbWWjp9%nVR=5zOT1LqPmWlmo@0eOxy3Rog8T&2E)#3~ zpE$eVW-`#x;W6OZt^m1Dqz12HYh?WgzzSx`HzvNYC-3>RV?-0T!eL1CW|DorO_#Jl9@zZI6i~Ac0SWq8;+L@?5t( zrvjIhK|=Co%ZH;*3NT=fbX+uP)Ut5q!PvXxu69?yPu8_{eVx<|rrjytHa7Ilk@TK7 z(;vW^$LOSLonOf;+=T3;;^r>yNS2Vj3A(x4rc8+Cz8l-S$0pi)pzYPQp)>s)$jI`J zU;|G;HJ#it_R8b;aMfd+QAn=2lG@-?U@r|1W{I{O;WWAAf;OSHr~`l6 zghFb@P3i@%A2VK4%h0x;sePf0AWwHOqxc$D>OQVY6(xlxD(ypsw7mijuEL%n*eGZn zs-A(r0`2$tsuM`%Bs#Dwou~}yH4auq5+poO!FIV+hXN1VUa*|1t3(z=!S`{66U*@D zqf}{JBWA3(g zYgq5oH)rAG!}v^<;df zTWYkg+e%)DW8i~8^o-C?N;{oS3K(Nv04RUMN;k3L;BC~fYlpdWe&_<^yf>#rXHm&j zAkV)i-vg9vO~3YZy7FYY^6*4&dcHqBzg~VeL;ExHTN5X?CSKo~nEqEG_e$yC1jvBo zjvr3e#|I^H{}1}<&Z{HzyToNI0LvU&xfX{0 zX&OmkQ5m;HmguW*GNh!8v33+O~w9p3|3K(zX>t{@Tha3{b?xIX}59?hAmxnq5t%2bM%7nwU!DR$4FzDh(moF^P zP+@0^*!5T_x4a-tGcOF%?OX_W*6`9q&)Q@lb#Gac`>D zpQ?3>HI-pt34(wyfmnvHuwOx(ZwbQnJ5zjl2g@gYMHu0WtXQH z#dFP&eR6zsYS)xZk28O0-TjZsV`fZV^j{0qp$&UJMk#a}?J?Ecp7erESCEh*l9a&}y!19Fh4)A=tOSS5Y{ literal 0 HcmV?d00001 diff --git a/main.py b/main.py index 8391945..ace19f8 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import asyncio import didkit import json from jinja2 import Environment, FileSystemLoader, select_autoescape +import idhub_ssikit jwk_issuer = didkit.generate_ed25519_key() jwk_subject = didkit.generate_ed25519_key() @@ -10,24 +11,22 @@ did_issuer = didkit.key_to_did("key", jwk_issuer) did_subject = didkit.key_to_did("key", jwk_subject) -async def main(): - env = Environment( - loader=FileSystemLoader("vc_templates"), - autoescape=select_autoescape() - ) - unsigned_vc_template = env.get_template("affiliation.jsonld.j2") - data = { - "vc_id": "http://example.org/credentials/3731", - "issuer_did": did_issuer, - "subject_did": did_subject, - "issuance_date": "2020-08-19T21:41:50Z", - "subject_is_member_of": "Pangea" - } - unsigned_vc = unsigned_vc_template.render(data) - signed_credential = await didkit.issue_credential( - unsigned_vc, - json.dumps({"proofFormat": "ldp"}), - jwk_issuer) - print(signed_credential) +env = Environment( + loader=FileSystemLoader("vc_templates"), + autoescape=select_autoescape() +) +unsigned_vc_template = env.get_template("member-credential.json") +data = { + "vc_id": "http://example.org/credentials/3731", + "issuer_did": did_issuer, + "subject_did": did_subject, + "issuance_date": "2020-08-19T21:41:50Z", + "subject_is_member_of": "Pangea" +} +signed_credential = idhub_ssikit.render_and_sign_credential( + unsigned_vc_template, + jwk_issuer, + data +) -asyncio.run(main()) \ No newline at end of file +print(signed_credential) diff --git a/ssikit_trustchain/__init__.py b/ssikit_trustchain/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ssikit_trustchain/__pycache__/__init__.cpython-311.pyc b/ssikit_trustchain/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 0cae79f6ad44ae0c2452f0903758788769a7fa00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 188 zcmZ3^%ge<81PPsHsUZ3>h=2h`DC095kTIPhg&~+hlhJP_LlF~@{~09t%U?euKQ~oB zB{45EHAg?7GC3o$C^w)eKPxr4q*%W^F()UrBs;StzNDzMxCAJfnWtY|oQWn9AD@{A rR1>dPQ2C3)CO1E&G$+-rh!to)$OXmxK;i>4BO~Jn1{hJq3={(ZpQkY1 diff --git a/vc_templates/academic.jsonld.j2 b/vc_templates/academic.jsonld.j2 index 8805101..1489d49 100644 --- a/vc_templates/academic.jsonld.j2 +++ b/vc_templates/academic.jsonld.j2 @@ -4,7 +4,7 @@ { "title" : "trustchain:title", "name": "trustchain:name", - "grade": "trustchain:grade" + "grade": "trustchain:grade", "date_earned": "trustchain:dateearned" } ], diff --git a/vc_templates/member-credential.json b/vc_templates/member-credential.json new file mode 100644 index 0000000..e98bef2 --- /dev/null +++ b/vc_templates/member-credential.json @@ -0,0 +1,32 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "name": "https://schema.org/name", + "email": "https://schema.org/email", + "membershipType": "https://schema.org/memberOf", + "individual": "https://schema.org/Person", + "organization": "https://schema.org/Organization", + "Member": "https://schema.org/Member", + "startDate": "https://schema.org/startDate", + "jsonSchema": "https://schema.org/jsonSchema", + "$ref": "https://schema.org/jsonSchemaRef" + } + ], + "type": ["VerifiableCredential", "VerifiableAttestation"], + "id": "{{ vc_id }}", + "issuer": "{{ issuer_did }}", + "issuanceDate": "{{ issuance_date }}", + "credentialSubject": { + "id": "{{ subject_did }}", + "Member": { + "name": "{{ name }}", + "email": "{{ email }}", + "membershipType": "{{ membershipType }}", + "startDate": "{{ startDate }}" + }, + "jsonSchema": { + "$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/member-schema.json" + } + } +} \ No newline at end of file diff --git a/vc_templates/wikipedia.jsonld.j2 b/vc_templates/wikipedia.jsonld.j2 new file mode 100644 index 0000000..398a290 --- /dev/null +++ b/vc_templates/wikipedia.jsonld.j2 @@ -0,0 +1,23 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "0892f680-6aeb-11eb-9bcf-f10d8993fde7", + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "issuer": { + "id": "{{ issuer_did }}", + "name": "Acme University" + }, + "issuanceDate": "2021-05-11T23:09:06.803Z", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science" + } + } +} \ No newline at end of file