Limpiado el sistema de tests previo a integración en la pipeline automática

This commit is contained in:
daniel 2024-02-22 12:09:08 +01:00
parent 594c1854a5
commit d269adb2aa
3 changed files with 157 additions and 161 deletions

View file

@ -1,18 +1,50 @@
import asyncio import asyncio
import base64 import base64
import datetime import copy
import zlib import zlib
from typing import Any from typing import Callable, Any
import didkit import didkit
import json import json
import jinja2
from jinja2 import Environment, FileSystemLoader, select_autoescape
from ast import literal_eval from ast import literal_eval
from pyroaring import BitMap from pyroaring import BitMap
def deep_merge_dict_inplace(d1: dict, d2: dict):
"""
Implements d1 |= d2, but recursively.
Merges d1 and d2, giving preference to keys in d2.
Keys in d1 but not in d2 are left as-is.
"""
for key, val in d2.items():
if isinstance(d1.get(key, None), dict) and isinstance(val, dict):
deep_merge_dict_inplace(d1[key], val)
continue
d1[key] = val
def deep_merge_dict(d1: dict, d2: dict) -> dict:
"""
Implements d1 | d2, but recursively.
Merges d1 and d2, giving preference to keys in d2.
Keys in d1 but not in d2 are left as-is.
"""
d1 = copy.deepcopy(d1)
deep_merge_dict_inplace(d1, d2)
return d1
def deep_filter_dict(f: Callable[[Any], bool], d: dict):
"""
Implements builtin filter(), but recursively.
Applies f to all k,v pairs in d. If some v is a dict, recurse into v instead of applying f(v) directly.
"""
for key, val in d.items():
if isinstance(val, dict):
yield key, dict(deep_filter_dict(f, val))
elif f(val):
yield key, val
def generate_did_controller_key(): def generate_did_controller_key():
return didkit.generate_ed25519_key() return didkit.generate_ed25519_key()
@ -21,11 +53,6 @@ def keydid_from_controller_key(key):
return didkit.key_to_did("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 resolve_did(keydid): def resolve_did(keydid):
async def inner(): async def inner():
return await didkit.resolve_did(keydid, "{}") return await didkit.resolve_did(keydid, "{}")
@ -81,21 +108,6 @@ def render_and_sign_credential(unsigned_vc: dict, jwk_issuer):
return asyncio.run(inner()) return asyncio.run(inner())
def sign_credential(unsigned_vc: str, jwk_issuer):
"""
Signs the unsigned credential with the provided key.
"""
async def inner():
signed_vc = await didkit.issue_credential(
unsigned_vc,
'{"proofFormat": "ldp"}',
jwk_issuer
)
return signed_vc
return asyncio.run(inner())
def verify_credential(vc): def verify_credential(vc):
""" """
Returns a (bool, str) tuple indicating whether the credential is valid. Returns a (bool, str) tuple indicating whether the credential is valid.
@ -103,7 +115,10 @@ def verify_credential(vc):
If it is false, the VC is invalid and the second argument contains a JSON object with further information. If it is false, the VC is invalid and the second argument contains a JSON object with further information.
""" """
async def inner(): async def inner():
try:
str_res = await didkit.verify_credential(vc, '{"proofFormat": "ldp"}') str_res = await didkit.verify_credential(vc, '{"proofFormat": "ldp"}')
except:
return False, "Invalid, corrupt, or tampered-with credential."
res = literal_eval(str_res) res = literal_eval(str_res)
ok = res["warnings"] == [] and res["errors"] == [] ok = res["warnings"] == [] and res["errors"] == []
return ok, str_res return ok, str_res
@ -117,8 +132,10 @@ def verify_credential(vc):
# Credential verifies against its schema. Now check revocation status. # Credential verifies against its schema. Now check revocation status.
vc = json.loads(vc) vc = json.loads(vc)
if "credentialStatus" in vc: if "credentialStatus" in vc:
vc_issuer = vc["credentialStatus"]["id"] # Either a DID:WEB or the special value in the line below
if vc_issuer == "https://revocation.not.supported/":
return True, "This credential does not support revocation"
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. 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
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_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] issuer_revocation_list = issuer_did_document["service"][0]
assert issuer_revocation_list["type"] == "RevocationBitmap2022" assert issuer_revocation_list["type"] == "RevocationBitmap2022"
@ -135,9 +152,8 @@ def verify_credential(vc):
return True, "Credential passes all checks" return True, "Credential passes all checks"
def issue_verifiable_presentation(vc_list: list[str], jwk_holder: str, holder_did: str) -> str: def issue_verifiable_presentation(vc_list: list[str], jwk_holder: str, holder_did: str, presentation_id: str) -> str:
async def inner(): async def inner(unsigned_vp):
unsigned_vp = unsigned_vp_template.render(data)
signed_vp = await didkit.issue_presentation( signed_vp = await didkit.issue_presentation(
unsigned_vp, unsigned_vp,
'{"proofFormat": "ldp"}', '{"proofFormat": "ldp"}',
@ -145,27 +161,40 @@ def issue_verifiable_presentation(vc_list: list[str], jwk_holder: str, holder_di
) )
return signed_vp return signed_vp
env = Environment( unsigned_vp = json.dumps({
loader=FileSystemLoader("vc_templates"), "@context": [
autoescape=select_autoescape() "https://www.w3.org/2018/credentials/v1"
) ],
unsigned_vp_template = env.get_template("verifiable_presentation.json") "id": presentation_id,
data = { "type": [
"holder_did": holder_did, "VerifiablePresentation"
"verifiable_credential_list": "[" + ",".join(vc_list) + "]" ],
} "holder": holder_did,
"verifiableCredential": vc_list
})
return asyncio.run(inner()) return asyncio.run(inner(unsigned_vp))
def verify_presentation(vp): def verify_presentation(vp: str):
""" """
Returns a (bool, str) tuple indicating whether the presentation is valid. Returns a (bool, str) tuple indicating whether the presentation is valid.
If the boolean is true, the presentation is valid and the second argument can be ignored. If the boolean is true, the presentation 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. If it is false, the VC is invalid and the second argument contains further information.
""" """
async def inner(): async def inner():
proof_options = '{"proofFormat": "ldp"}' str_res = await didkit.verify_presentation(vp, '{"proofFormat": "ldp"}')
return await didkit.verify_presentation(vp, proof_options) res = literal_eval(str_res)
ok = res["warnings"] == [] and res["errors"] == []
return ok, str_res
return asyncio.run(inner()) valid, reason = asyncio.run(inner())
if not valid:
return valid, f"Presentation is invalid: {reason}"
vp = json.loads(vp)
for idx, credential in enumerate(vp["verifiableCredential"]):
valid, reason = verify_credential(json.dumps(credential))
if not valid:
return valid, f"Credential at index {idx} is invalid with reason: {reason}"
# Fallthrough means all is good.
return True, "Verifiable presentation passes all checks"

193
main.py
View file

@ -1,50 +1,11 @@
import asyncio
from typing import Callable, Any
import didkit import didkit
import json import json
from jinja2 import Environment, FileSystemLoader, select_autoescape
import idhub_ssikit import idhub_ssikit
from ast import literal_eval import logging
import copy from sys import stderr
def deep_merge_dict_inplace(d1: dict, d2: dict): def test_all_use_cases():
"""
Implements d1 |= d2, but recursively.
Merges d1 and d2, giving preference to keys in d2.
Keys in d1 but not in d2 are left as-is.
"""
for key, val in d2.items():
if isinstance(d1.get(key, None), dict) and isinstance(val, dict):
deep_merge_dict_inplace(d1[key], val)
continue
d1[key] = val
def deep_merge_dict(d1: dict, d2: dict) -> dict:
"""
Implements d1 | d2, but recursively.
Merges d1 and d2, giving preference to keys in d2.
Keys in d1 but not in d2 are left as-is.
"""
d1 = copy.deepcopy(d1)
deep_merge_dict_inplace(d1, d2)
return d1
def deep_filter_dict(f: Callable[[Any], bool], d: dict):
"""
Implements builtin filter(), but recursively.
Applies f to all k,v pairs in d. If some v is a dict, recurse into v instead of applying f(v) directly.
"""
for key, val in d.items():
if isinstance(val, dict):
yield key, dict(deep_filter_dict(f, val))
elif f(val):
yield key, val
def test_all_vcs(use_webdid=False):
vcs = [ vcs = [
'membership-card', 'membership-card',
'financial-vulnerability', 'financial-vulnerability',
@ -52,24 +13,71 @@ def test_all_vcs(use_webdid=False):
'federation-membership', 'federation-membership',
'e-operator-claim' 'e-operator-claim'
] ]
# Test basic VC issuance: did:key, no revocation checks (they are not supported with did:key)
for vc in vcs: for vc in vcs:
print(f"trying {vc}... ", end="") print(f"trying {vc} in did:key mode... ", end="", file=stderr)
try: try:
signed_cred = issue_vc_test_newstyle(vc, use_webdid) signed_cred = issue_vc_test_newstyle(vc, use_web=False)
ok, err = idhub_ssikit.verify_credential(signed_cred) ok, err = idhub_ssikit.verify_credential(signed_cred)
if ok: if ok:
print("OK") print("OK", file=stderr)
else: else:
print("FAILED!", err) print("FAILED!", err, file=stderr)
open(f'/tmp/{vc}', mode='w').write(signed_cred)
except Exception as e: except Exception as e:
print("FAILED! With exception:") logging.exception("FAILED! With exception:")
print(e) # Test VC issuance using did:web DIDs for the issuer, unrevoked, and check revocation
print("", file=stderr)
for vc in vcs:
print(f"trying {vc} in did:web mode, unrevoked... ", end="", file=stderr)
try:
signed_cred = issue_vc_test_newstyle(vc, use_web=True, did_revokes_vc=False)
ok, err = idhub_ssikit.verify_credential(signed_cred)
if ok:
print("OK", file=stderr)
else:
print("FAILED!", err, file=stderr)
except Exception as e:
logging.exception("FAILED! With exception:")
# Test VC issuance using did:web DIDs for the issuer, *revoked*, and check revocation
print("", file=stderr)
for vc in vcs:
print(f"trying {vc} in did:web mode, *REVOKED*... ", end="", file=stderr)
try:
signed_cred = issue_vc_test_newstyle(vc, use_web=True, did_revokes_vc=True)
ok, err = idhub_ssikit.verify_credential(signed_cred)
if not ok:
print("OK", file=stderr)
else:
print("FAILED! Credential ", err, file=stderr)
except Exception as e:
logging.exception("FAILED! With exception:")
# Test VC resistance to tampering by modifying a credential after signature
print("", file=stderr)
for vc in vcs:
print(f"tampering {vc} after issuance, check that it fails to verify... ", end="", file=stderr)
try:
issue_vc_test_and_fail_verification(vc) # All went well if this doesn't raise an exception
print("OK", file=stderr)
except Exception as e:
logging.exception("FAILED!")
# Test VP issuance and signature
print("", file=stderr)
print(f"doing end-to-end VP test, expected success... ", end="", file=stderr)
ok, reason = issue_and_sign_vp_test(revoked_credential=False)
assert ok is True
print("OK", file=stderr)
print(f"doing end-to-end VP test, expected failure... ", end="", file=stderr)
ok, reason = issue_and_sign_vp_test(revoked_credential=True)
assert ok is False
print("OK", file=stderr)
def issue_vc_test_newstyle(vc_name, use_web=True, did_revokes_vc=False, check_revocation=False): def issue_vc_test_newstyle(vc_name,
use_web=True,
did_revokes_vc=False,
holder_jwk=None):
jwk_issuer = '{"kty":"OKP","crv":"Ed25519","x":"piojLFIHQ4Z6heRuPI87nrfMJKdet1dJIPG15iGjmDE","d":"zpOBTDrp_iNQTY5nZlIxLA34Sl7FXWXNGehFktznxTM"}' jwk_issuer = '{"kty":"OKP","crv":"Ed25519","x":"piojLFIHQ4Z6heRuPI87nrfMJKdet1dJIPG15iGjmDE","d":"zpOBTDrp_iNQTY5nZlIxLA34Sl7FXWXNGehFktznxTM"}'
jwk_subject = '{"kty":"OKP","crv":"Ed25519","x":"BuKyt44QKYSX6kmAt771ai37lIFNwYlhugWXPiqcyYU","d":"qbvMhSCPKvQ-vSkqNr3q8gWY5zPUj7ry0t2YnmT7agc"}' jwk_subject = holder_jwk or '{"kty":"OKP","crv":"Ed25519","x":"BuKyt44QKYSX6kmAt771ai37lIFNwYlhugWXPiqcyYU","d":"qbvMhSCPKvQ-vSkqNr3q8gWY5zPUj7ry0t2YnmT7agc"}'
did_subject = didkit.key_to_did("key", jwk_subject) did_subject = didkit.key_to_did("key", jwk_subject)
if use_web: if use_web:
if did_revokes_vc: if did_revokes_vc:
@ -81,39 +89,34 @@ def issue_vc_test_newstyle(vc_name, use_web=True, did_revokes_vc=False, check_re
vc_template = json.load(open(f'schemas/vc_templates/{vc_name}.json')) vc_template = json.load(open(f'schemas/vc_templates/{vc_name}.json'))
data_base = json.load(open(f'schemas/vc_examples/base--data.json')) data_base = json.load(open(f'schemas/vc_examples/base--data.json'))
data_specific = json.load(open(f'schemas/vc_examples/{vc_name}--data.json')) data_specific = json.load(open(f'schemas/vc_examples/{vc_name}--data.json'))
data = deep_merge_dict(data_base, data_specific) data = idhub_ssikit.deep_merge_dict(data_base, data_specific)
data["issuer"]["id"] = did_issuer data["issuer"]["id"] = did_issuer
data["credentialSubject"]["id"] = did_subject data["credentialSubject"]["id"] = did_subject
if use_web:
data["credentialStatus"]["id"] = did_issuer data["credentialStatus"]["id"] = did_issuer
data["credentialStatus"]["revocationBitmapIndex"] = "420" data["credentialStatus"]["revocationBitmapIndex"] = 42
vc_rendered_unsigned = deep_merge_dict(vc_template, data) vc_rendered_unsigned = idhub_ssikit.deep_merge_dict(vc_template, data)
signed_credential = idhub_ssikit.render_and_sign_credential( signed_credential = idhub_ssikit.render_and_sign_credential(
vc_rendered_unsigned, vc_rendered_unsigned,
jwk_issuer, jwk_issuer,
) )
if check_revocation:
assert use_web is True
ok, reason = idhub_ssikit.verify_credential(signed_credential)
print(ok)
print(reason)
return signed_credential return signed_credential
def issue_vc_test_and_fail_verification(vc_name): def issue_vc_test_and_fail_verification(vc_name):
signed_credential = issue_vc_test_newstyle(vc_name)
verification_result = idhub_ssikit.verify_credential(signed_credential)
print(verification_result)
def replace(s, position, character): def replace(s, position, character):
return s[:position] + character + s[position+1:] return s[:position] + character + s[position+1:]
signed_credential = issue_vc_test_newstyle(vc_name)
ok, reason = idhub_ssikit.verify_credential(signed_credential)
assert ok is True, (ok, reason)
signed_credential = replace(signed_credential, (len(signed_credential)//4)*3, ".") signed_credential = replace(signed_credential, (len(signed_credential)//4)*3, ".")
verification_result = idhub_ssikit.verify_credential(signed_credential) ok, reason = idhub_ssikit.verify_credential(signed_credential)
print(verification_result) assert ok is False, (ok, reason)
def issue_and_sign_vp_test(): def issue_and_sign_vp_test(revoked_credential=False):
""" """
In this example execution two Verifiable Credentials associated with a single Holder are issued and then In this example execution two Verifiable Credentials associated with a single Holder are issued and then
combined into a single Verifiable Presentation. combined into a single Verifiable Presentation.
@ -123,51 +126,15 @@ def issue_and_sign_vp_test():
- Issuer B being "EXO" foundation, - Issuer B being "EXO" foundation,
- Verifier (not pictured) being "Som Connexio", which wants verifiable data of the Holder from both Issuers. - Verifier (not pictured) being "Som Connexio", which wants verifiable data of the Holder from both Issuers.
""" """
jwk_issuer = didkit.generate_ed25519_key() holder_jwk = didkit.generate_ed25519_key()
jwk_issuer2 = didkit.generate_ed25519_key() holder_did = didkit.key_to_did("key", holder_jwk)
jwk_subject = didkit.generate_ed25519_key() vc1 = issue_vc_test_newstyle('membership-card', use_web=True, holder_jwk=holder_jwk)
vc2 = issue_vc_test_newstyle('course-credential', use_web=True, did_revokes_vc=revoked_credential, holder_jwk=holder_jwk)
did_issuer = didkit.key_to_did("key", jwk_issuer) signed_presentation = idhub_ssikit.issue_verifiable_presentation(
did_issuer2 = didkit.key_to_did("key", jwk_issuer2) [json.loads(vc1), json.loads(vc2)],
did_subject = didkit.key_to_did("key", jwk_subject) holder_jwk,
print(did_issuer) holder_did,
print(did_issuer2) "https://idhub.pangea.org/presentations/42"
print(did_subject)
# TODO: WE'RE NO LONGER USING JINJA2
env = Environment(
loader=FileSystemLoader("vc_templates"),
autoescape=select_autoescape()
) )
unsigned_vc_template = env.get_template("member.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
)
data2 = data
data2["issuer_did"] = did_issuer2
signed_credential2 = idhub_ssikit.render_and_sign_credential(
unsigned_vc_template,
jwk_issuer2,
data2
)
signed_presentation = idhub_ssikit.issue_verifiable_presentation([signed_credential, signed_credential2], jwk_subject, did_subject)
print("##############--- SIGNED PRESENTATION ---##############")
print(signed_presentation)
print("##############--- ------------------- ---##############")
res = idhub_ssikit.verify_presentation(signed_presentation) return idhub_ssikit.verify_presentation(signed_presentation)
print(res)
def scratch():
jwk_issuer = didkit.generate_ed25519_key()
did_issuer = didkit.key_to_did("key", jwk_issuer)

@ -1 +1 @@
Subproject commit f1e6a33d0801a2104bf297292b953b1d138f675b Subproject commit 90ce148f88e18dead0d9b2f94803236d6312ee09