First iteration of the ssi routines
This commit is contained in:
parent
440ea7ebbe
commit
9608add9dd
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.idea/
|
73
README.md
73
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
|
||||||
|
)
|
||||||
|
```
|
58
idhub_ssikit/__init__.py
Normal file
58
idhub_ssikit/__init__.py
Normal file
|
@ -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())
|
BIN
idhub_ssikit/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
idhub_ssikit/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
39
main.py
39
main.py
|
@ -2,6 +2,7 @@ import asyncio
|
||||||
import didkit
|
import didkit
|
||||||
import json
|
import json
|
||||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
|
import idhub_ssikit
|
||||||
|
|
||||||
jwk_issuer = didkit.generate_ed25519_key()
|
jwk_issuer = didkit.generate_ed25519_key()
|
||||||
jwk_subject = 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)
|
did_subject = didkit.key_to_did("key", jwk_subject)
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
env = Environment(
|
||||||
env = Environment(
|
loader=FileSystemLoader("vc_templates"),
|
||||||
loader=FileSystemLoader("vc_templates"),
|
autoescape=select_autoescape()
|
||||||
autoescape=select_autoescape()
|
)
|
||||||
)
|
unsigned_vc_template = env.get_template("member-credential.json")
|
||||||
unsigned_vc_template = env.get_template("affiliation.jsonld.j2")
|
data = {
|
||||||
data = {
|
"vc_id": "http://example.org/credentials/3731",
|
||||||
"vc_id": "http://example.org/credentials/3731",
|
"issuer_did": did_issuer,
|
||||||
"issuer_did": did_issuer,
|
"subject_did": did_subject,
|
||||||
"subject_did": did_subject,
|
"issuance_date": "2020-08-19T21:41:50Z",
|
||||||
"issuance_date": "2020-08-19T21:41:50Z",
|
"subject_is_member_of": "Pangea"
|
||||||
"subject_is_member_of": "Pangea"
|
}
|
||||||
}
|
signed_credential = idhub_ssikit.render_and_sign_credential(
|
||||||
unsigned_vc = unsigned_vc_template.render(data)
|
unsigned_vc_template,
|
||||||
signed_credential = await didkit.issue_credential(
|
jwk_issuer,
|
||||||
unsigned_vc,
|
data
|
||||||
json.dumps({"proofFormat": "ldp"}),
|
)
|
||||||
jwk_issuer)
|
|
||||||
print(signed_credential)
|
|
||||||
|
|
||||||
asyncio.run(main())
|
print(signed_credential)
|
||||||
|
|
Binary file not shown.
|
@ -4,7 +4,7 @@
|
||||||
{
|
{
|
||||||
"title" : "trustchain:title",
|
"title" : "trustchain:title",
|
||||||
"name": "trustchain:name",
|
"name": "trustchain:name",
|
||||||
"grade": "trustchain:grade"
|
"grade": "trustchain:grade",
|
||||||
"date_earned": "trustchain:dateearned"
|
"date_earned": "trustchain:dateearned"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
32
vc_templates/member-credential.json
Normal file
32
vc_templates/member-credential.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
vc_templates/wikipedia.jsonld.j2
Normal file
23
vc_templates/wikipedia.jsonld.j2
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in a new issue