2023-11-10 05:48:52 +00:00
import asyncio
2024-02-01 20:19:07 +00:00
import base64
2023-11-10 05:48:52 +00:00
import datetime
2024-02-01 20:19:07 +00:00
import zlib
2024-02-05 18:44:54 +00:00
from ast import literal_eval
2024-02-01 20:19:07 +00:00
2023-11-10 05:48:52 +00:00
import didkit
import json
2024-01-20 10:36:45 +00:00
import urllib
2023-11-10 05:48:52 +00:00
import jinja2
2023-11-15 10:43:13 +00:00
from django . template . backends . django import Template
2023-12-01 18:31:09 +00:00
from django . template . loader import get_template
2024-02-01 20:19:07 +00:00
from pyroaring import BitMap
2023-11-10 05:48:52 +00:00
2024-01-15 09:34:42 +00:00
from trustchain_idhub import settings
2023-11-10 05:48:52 +00:00
def generate_did_controller_key ( ) :
return didkit . generate_ed25519_key ( )
def keydid_from_controller_key ( key ) :
return didkit . key_to_did ( " key " , key )
2024-02-01 20:19:07 +00:00
def resolve_did ( keydid ) :
async def inner ( ) :
return await didkit . resolve_did ( keydid , " {} " )
return asyncio . run ( inner ( ) )
2024-01-15 09:34:42 +00:00
def webdid_from_controller_key ( key ) :
"""
Se siguen los pasos para generar un webdid a partir de un keydid .
Documentado en la docu de spruceid .
"""
keydid = keydid_from_controller_key ( key ) # "did:key:<...>"
pubkeyid = keydid . rsplit ( " : " ) [ - 1 ] # <...>
2024-02-01 20:19:07 +00:00
document = json . loads ( resolve_did ( keydid ) ) # Documento DID en terminos "key"
2024-01-20 10:36:45 +00:00
domain = urllib . parse . urlencode ( { " domain " : settings . DOMAIN } ) [ 7 : ]
webdid_url = f " did:web: { domain } :did-registry: { pubkeyid } " # nueva URL: "did:web:idhub.pangea.org:<...>"
2024-01-15 09:34:42 +00:00
webdid_url_owner = webdid_url + " #owner "
# Reemplazamos los campos del documento DID necesarios:
document [ " id " ] = webdid_url
2024-01-16 13:01:15 +00:00
document [ " verificationMethod " ] [ 0 ] [ " id " ] = webdid_url_owner
document [ " verificationMethod " ] [ 0 ] [ " controller " ] = webdid_url
document [ " authentication " ] [ 0 ] = webdid_url_owner
document [ " assertionMethod " ] [ 0 ] = webdid_url_owner
2024-01-15 09:34:42 +00:00
document_fixed_serialized = json . dumps ( document )
return webdid_url , document_fixed_serialized
2023-11-10 05:48:52 +00:00
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 ( ) )
2023-11-15 10:43:13 +00:00
def sign_credential ( unsigned_vc : str , jwk_issuer ) :
"""
2023-12-01 18:31:09 +00:00
Signs the unsigned credential with the provided key .
The credential template must be rendered with all user data .
2023-11-15 10:43:13 +00:00
"""
async def inner ( ) :
2023-12-01 18:31:09 +00:00
signed_vc = await didkit . issue_credential (
unsigned_vc ,
' { " proofFormat " : " ldp " } ' ,
jwk_issuer
)
return signed_vc
2023-11-15 10:43:13 +00:00
return asyncio . run ( inner ( ) )
2023-11-10 05:48:52 +00:00
2023-12-01 18:31:09 +00:00
def verify_credential ( vc ) :
2023-11-10 05:48:52 +00:00
"""
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 ( ) :
2024-02-05 18:44:54 +00:00
str_res = await didkit . verify_credential ( vc , ' { " proofFormat " : " ldp " } ' )
res = literal_eval ( str_res )
ok = res [ " warnings " ] == [ ] and res [ " errors " ] == [ ]
return ok , str_res
2023-11-10 05:48:52 +00:00
2024-01-31 09:54:40 +00:00
valid , reason = asyncio . run ( inner ( ) )
if not valid :
return valid , reason
# Credential passes basic signature verification. Now check it against its schema.
2024-02-01 20:19:07 +00:00
# TODO: check agasint schema
2024-02-14 17:35:34 +00:00
# pass
2024-02-01 20:19:07 +00:00
# Credential verifies against its schema. Now check revocation status.
vc = json . loads ( vc )
2024-02-05 18:44:54 +00:00
if " credentialStatus " in vc :
revocation_index = int ( vc [ " credentialStatus " ] [ " revocationBitmapIndex " ] ) # NOTE: THIS FIELD SHOULD BE SERIALIZED AS AN INTEGER, BUT IOTA DOCUMENTAITON SERIALIZES IT AS A STRING. DEFENSIVE CAST ADDED JUST IN CASE.
vc_issuer = vc [ " issuer " ] [ " id " ] # This is a DID
if vc_issuer [ : 7 ] == " did:web " : # Only DID:WEB can revoke
issuer_did_document = json . loads ( resolve_did ( vc_issuer ) ) # TODO: implement a caching layer so we don't have to fetch the DID (and thus the revocation list) every time a VC is validated.
issuer_revocation_list = issuer_did_document [ " service " ] [ 0 ]
assert issuer_revocation_list [ " type " ] == " RevocationBitmap2022 "
revocation_bitmap = BitMap . deserialize (
zlib . decompress (
base64 . b64decode (
2024-02-14 17:35:34 +00:00
issuer_revocation_list [ " serviceEndpoint " ] . rsplit ( " , " ) [ 1 ] . encode ( ' utf-8 ' )
2024-02-05 18:44:54 +00:00
)
)
2024-02-01 20:19:07 +00:00
)
2024-02-05 18:44:54 +00:00
if revocation_index in revocation_bitmap :
return False , " Credential has been revoked by the issuer "
2024-02-01 20:19:07 +00:00
# Fallthrough means all is good.
2024-02-05 18:44:54 +00:00
return True , " Credential passes all checks "
2023-11-27 06:42:12 +00:00
2023-12-01 18:31:09 +00:00
def issue_verifiable_presentation ( vp_template : Template , vc_list : list [ str ] , jwk_holder : str , holder_did : str ) - > str :
2023-11-27 06:42:12 +00:00
async def inner ( ) :
2023-12-01 18:31:09 +00:00
unsigned_vp = vp_template . render ( data )
2023-11-27 06:42:12 +00:00
signed_vp = await didkit . issue_presentation (
unsigned_vp ,
' { " proofFormat " : " ldp " } ' ,
jwk_holder
)
return signed_vp
data = {
" holder_did " : holder_did ,
" verifiable_credential_list " : " [ " + " , " . join ( vc_list ) + " ] "
}
return asyncio . run ( inner ( ) )
2023-12-04 09:56:22 +00:00
def create_verifiable_presentation ( jwk_holder : str , unsigned_vp : str ) - > str :
async def inner ( ) :
signed_vp = await didkit . issue_presentation (
unsigned_vp ,
' { " proofFormat " : " ldp " } ' ,
jwk_holder
)
return signed_vp
return asyncio . run ( inner ( ) )
2023-11-27 06:42:12 +00:00
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 " } '
2023-12-01 18:31:09 +00:00
return await didkit . verify_presentation ( vp , proof_options )
2023-11-27 06:42:12 +00:00
return asyncio . run ( inner ( ) )
2023-12-01 18:31:09 +00:00