Merge branch 'ssi' into oidc4vp

This commit is contained in:
Cayo Puigdefabregas 2023-11-27 09:22:58 +01:00
commit bc177bbd62
11 changed files with 292 additions and 0 deletions

View file

@ -0,0 +1,30 @@
{
"@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",
"street_address": "https://schema.org/streetAddress",
"connectivity_option_list": "https://schema.org/connectivityOptionList",
"$ref": "https://schema.org/jsonSchemaRef"
}
],
"id": "{{ vc_id }}",
"type": ["VerifiableCredential", "HomeConnectivitySurveyCredential"],
"issuer": "{{ issuer_did }}",
"issuanceDate": "{{ issuance_date }}",
"credentialSubject": {
"id": "{{ subject_did }}",
"street_address": "{{ street_address }}",
"connectivity_option_list": "{{ connectivity_option_list }}",
"jsonSchema": {
"$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json"
}
}
}

View file

@ -0,0 +1,33 @@
{
"@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",
"destination_country": "https://schema.org/destinationCountry",
"offboarding_date": "https://schema.org/offboardingDate",
"$ref": "https://schema.org/jsonSchemaRef"
}
],
"id": "{{ vc_id }}",
"type": ["VerifiableCredential", "MigrantRescueCredential"],
"issuer": "{{ issuer_did }}",
"issuanceDate": "{{ issuance_date }}",
"credentialSubject": {
"id": "{{ subject_did }}",
"name": "{{ name }}",
"country_of_origin": "{{ country_of_origin }}",
"rescue_date": "{{ rescue_date }}",
"destination_country": "{{ destination_country }}",
"offboarding_date": "{{ offboarding_date }}",
"jsonSchema": {
"$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json"
}
}
}

View file

@ -0,0 +1,30 @@
{
"@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",
"street_address": "https://schema.org/streetAddress",
"financial_vulnerability_score": "https://schema.org/financialVulnerabilityScore",
"$ref": "https://schema.org/jsonSchemaRef"
}
],
"id": "{{ vc_id }}",
"type": ["VerifiableCredential", "FinancialSituationCredential"],
"issuer": "{{ issuer_did }}",
"issuanceDate": "{{ issuance_date }}",
"credentialSubject": {
"id": "{{ subject_did }}",
"street_address": "{{ street_address }}",
"financial_vulnerability_score": "{{ financial_vulnerability_score }}",
"jsonSchema": {
"$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json"
}
}
}

View file

@ -0,0 +1,11 @@
{
"@context": [
"https://www.w3.org/2018/credentials/v1"
],
"id": "http://example.org/presentations/3731",
"type": [
"VerifiablePresentation"
],
"holder": "{{ holder_did }}",
"verifiableCredential": {{ verifiable_credential_list }}
}

View file

@ -20,6 +20,7 @@ from django.urls import path, reverse_lazy
from .views import LoginView
from .admin import views as views_admin
from .user import views as views_user
from .verification_portal import views as views_verification_portal
app_name = 'idhub'
@ -171,4 +172,7 @@ urlpatterns = [
name='admin_import'),
path('admin/import/new', views_admin.ImportAddView.as_view(),
name='admin_import_add'),
path('verification_portal/verify/', views_verification_portal.verify,
name="verification_portal_verify")
]

View file

View file

@ -0,0 +1,24 @@
from django.db import models
class VPVerifyRequest(models.Model):
"""
`nonce` is an opaque random string used to lookup verification requests. URL-safe.
Example: "UPBQ3JE2DGJYHP5CPSCRIGTHRTCYXMQPNQ"
`expected_credentials` is a JSON list of credential types that must be present in this VP.
Example: ["FinancialSituationCredential", "HomeConnectivitySurveyCredential"]
`expected_contents` is a JSON object that places optional constraints on the contents of the
returned VP.
Example: [{"FinancialSituationCredential": {"financial_vulnerability_score": "7"}}]
`action` is (for now) a JSON object describing the next steps to take if this verification
is successful. For example "send mail to <destination> with <subject> and <body>"
Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...}
`response` is a URL that the user's wallet will redirect the user to.
`submitted_on` is used (by a cronjob) to purge old entries that didn't complete verification
"""
nonce = models.CharField(max_length=50)
expected_credentials = models.CharField(max_length=255)
expected_contents = models.TextField()
action = models.TextField()
response_or_redirect = models.CharField(max_length=255)
submitted_on = models.DateTimeField(auto_now=True)

View file

@ -0,0 +1,49 @@
import json
from django.core.mail import send_mail
from django.http import HttpResponse, HttpResponseRedirect
from utils.idhub_ssikit import verify_presentation
from .models import VPVerifyRequest
from django.shortcuts import get_object_or_404
from more_itertools import flatten, unique_everseen
def verify(request):
assert request.method == "POST"
# TODO: incorporate request.POST["presentation_submission"] as schema definition
(presentation_valid, _) = verify_presentation(request.POST["vp_token"])
if not presentation_valid:
raise Exception("Failed to verify signature on the given Verifiable Presentation.")
vp = json.loads(request.POST["vp_token"])
nonce = vp["nonce"]
# "vr" = verification_request
vr = get_object_or_404(VPVerifyRequest, nonce=nonce) # TODO: return meaningful error, not 404
# Get a list of all included verifiable credential types
included_credential_types = unique_everseen(flatten([
vc["type"] for vc in vp["verifiableCredential"]
]))
# Check that it matches what we requested
for requested_vc_type in json.loads(vr.expected_credentials):
if requested_vc_type not in included_credential_types:
raise Exception("You're missing some credentials we requested!") # TODO: return meaningful error
# Perform whatever action we have to do
action = json.loads(vr.action)
if action["action"] == "send_mail":
subject = action["params"]["subject"]
to_email = action["params"]["to"]
from_email = "noreply@verifier-portal"
body = request.POST["vp-token"]
send_mail(
subject,
body,
from_email,
[to_email]
)
elif action["action"] == "something-else":
pass
else:
raise Exception("Unknown action!")
# OK! Your verifiable presentation was successfully presented.
return HttpResponseRedirect(vr.response_or_redirect)

View file

@ -10,3 +10,4 @@ didkit==0.3.2
jinja2==3.1.2
jsonref==1.1.0
pyld==2.0.3
more-itertools==10.1.0

View file

@ -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
)
```

View file

@ -72,3 +72,40 @@ def verify_credential(vc, proof_options):
return didkit.verify_credential(vc, proof_options)
return asyncio.run(inner())
def issue_verifiable_presentation(vc_list: list[str], jwk_holder: str, holder_did: str) -> str:
async def inner():
unsigned_vp = unsigned_vp_template.render(data)
signed_vp = await didkit.issue_presentation(
unsigned_vp,
'{"proofFormat": "ldp"}',
jwk_holder
)
return signed_vp
# TODO: convert from Jinja2 -> django-templates
env = Environment(
loader=FileSystemLoader("vc_templates"),
autoescape=select_autoescape()
)
unsigned_vp_template = env.get_template("verifiable_presentation.json")
data = {
"holder_did": holder_did,
"verifiable_credential_list": "[" + ",".join(vc_list) + "]"
}
return asyncio.run(inner())
def verify_presentation(vp):
"""
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():
proof_options = '{"proofFormat": "ldp"}'
return didkit.verify_presentation(vp, proof_options)
return asyncio.run(inner())