Merge branch 'ssi' into oidc4vp
This commit is contained in:
commit
bc177bbd62
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ from django.urls import path, reverse_lazy
|
||||||
from .views import LoginView
|
from .views import LoginView
|
||||||
from .admin import views as views_admin
|
from .admin import views as views_admin
|
||||||
from .user import views as views_user
|
from .user import views as views_user
|
||||||
|
from .verification_portal import views as views_verification_portal
|
||||||
|
|
||||||
app_name = 'idhub'
|
app_name = 'idhub'
|
||||||
|
|
||||||
|
@ -171,4 +172,7 @@ urlpatterns = [
|
||||||
name='admin_import'),
|
name='admin_import'),
|
||||||
path('admin/import/new', views_admin.ImportAddView.as_view(),
|
path('admin/import/new', views_admin.ImportAddView.as_view(),
|
||||||
name='admin_import_add'),
|
name='admin_import_add'),
|
||||||
|
|
||||||
|
path('verification_portal/verify/', views_verification_portal.verify,
|
||||||
|
name="verification_portal_verify")
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
||||||
|
|
|
@ -10,3 +10,4 @@ didkit==0.3.2
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
jsonref==1.1.0
|
jsonref==1.1.0
|
||||||
pyld==2.0.3
|
pyld==2.0.3
|
||||||
|
more-itertools==10.1.0
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
```
|
|
@ -72,3 +72,40 @@ def verify_credential(vc, proof_options):
|
||||||
return didkit.verify_credential(vc, proof_options)
|
return didkit.verify_credential(vc, proof_options)
|
||||||
|
|
||||||
return asyncio.run(inner())
|
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())
|
||||||
|
|
Loading…
Reference in New Issue