Skip to content

Commit

Permalink
Merge pull request #238 from blockchain-certificates/feat/validate_me…
Browse files Browse the repository at this point in the history
…tadata

Feat/validate metadata
  • Loading branch information
lemoustachiste committed Jul 12, 2022
2 parents b268ac4 + 265df12 commit 76c99cb
Show file tree
Hide file tree
Showing 11 changed files with 382 additions and 131 deletions.
2 changes: 1 addition & 1 deletion cert_issuer/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '3.1.0'
__version__ = '3.2.0'
127 changes: 127 additions & 0 deletions cert_issuer/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import json
from abc import abstractmethod
from cert_issuer.config import ESTIMATE_NUM_INPUTS
from cert_issuer.models.verifiable_credential import verify_credential, verify_presentation, validate_type, validate_context
from cert_issuer.models.metadata import validate_metadata_structure

class BatchHandler(object):
def __init__(self, secret_manager, certificate_handler, merkle_tree, config):
self.certificate_handler = certificate_handler
self.secret_manager = secret_manager
self.merkle_tree = merkle_tree
self.config = config

@abstractmethod
def pre_batch_actions(self, config):
pass

@abstractmethod
def post_batch_actions(self, config):
pass

def set_certificates_in_batch(self, certificates_to_issue):
self.certificates_to_issue = certificates_to_issue


class CertificateHandler(object):
@abstractmethod
def validate_certificate(self, certificate_metadata):
validate_type(certificate_metadata['type'])
validate_context(certificate_metadata['@context'], certificate_metadata['type'])

if 'metadata' in certificate_metadata:
validate_metadata_structure(json.loads(certificate_metadata['metadata']))

if (certificate_metadata['type'][0] == 'VerifiableCredential'):
verify_credential(certificate_metadata)

if (certificate_metadata['type'][0] == 'VerifiablePresentation'):
verify_presentation(certificate_metadata)

pass

@abstractmethod
def sign_certificate(self, signer, certificate_metadata):
pass

@abstractmethod
def get_byte_array_to_issue(self, certificate_metadata):
pass

@abstractmethod
def add_proof(self, certificate_metadata, merkle_proof):
pass


class ServiceProviderConnector(object):
@abstractmethod
def get_balance(self, address):
pass

def broadcast_tx(self, tx):
pass


class Signer(object):
"""
Abstraction for a component that can sign.
"""

def __init__(self):
pass

@abstractmethod
def sign_message(self, wif, message_to_sign):
pass

@abstractmethod
def sign_transaction(self, wif, transaction_to_sign):
pass


class SecretManager(object):
def __init__(self, signer):
self.signer = signer
self.wif = None

@abstractmethod
def start(self):
pass

@abstractmethod
def stop(self):
pass

def sign_message(self, message_to_sign):
return self.signer.sign_message(self.wif, message_to_sign)

def sign_transaction(self, transaction_to_sign):
return self.signer.sign_transaction(self.wif, transaction_to_sign)


class TransactionHandler(object):
@abstractmethod
def ensure_balance(self):
pass

@abstractmethod
def issue_transaction(self, blockchain_bytes):
pass


class MockTransactionHandler(TransactionHandler):
def ensure_balance(self):
pass

def issue_transaction(self, op_return_bytes):
return 'This has not been issued on a blockchain and is for testing only'


class TransactionCreator(object):
@abstractmethod
def estimate_cost_for_certificate_batch(self, tx_cost_constants, num_inputs=ESTIMATE_NUM_INPUTS):
pass

@abstractmethod
def create_transaction(self, tx_cost_constants, issuing_address, inputs, op_return_value):
pass
70 changes: 70 additions & 0 deletions cert_issuer/models/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import logging

from jsonschema import validate
from copy import copy


def validate_metadata_structure(metadata):
if 'schema' in metadata:
try:
json_object = copy(metadata)
del json_object['schema']
validate(instance=json_object, schema=metadata['schema'])
except Exception as e:
print(e)
raise Exception('Certificate.metadata object does not match its provided schema')
else:
logging.warning("""
The metadata object provided with the certificate does not include a `schema` property.
Not defining such property will result in errors in the rendering of the metadata property in the UI projects.
""")

if 'displayOrder' not in metadata:
logging.warning("""
The metadata object provided with the certificate does not include a `displayOrder` property.
Not defining such property will result in errors in the rendering of the metadata property in the UI projects.
""")
return
else:
verify_display_order_properties(metadata)


def verify_display_order_properties(metadata):
display_order = metadata['displayOrder']
checked_groups = []
for item in display_order:
path = item.split('.')
group = path[0]
if group not in metadata:
if group not in checked_groups:
# \033[1m%s\033[0m: display property name in bold
logging.warning(
"`metadata.displayOrder` property references a group named: \033[1m%s\033[0m which does not exist in metadata object.",
group
)
checked_groups.append(group)
else:
property = path[1]
if property not in metadata[group]:
logging.warning(
"`metadata.displayOrder` property references a property named: \033[1m%s\033[0m which does not exist in group: \033[1m%s\033[0m.",
property,
group
)
else:
verify_title_is_set(property, group, metadata)

pass


def verify_title_is_set(property, group, metadata):
if 'schema' not in metadata:
return
schema = metadata['schema']

if 'title' not in schema['properties'][group]['properties'][property]:
logging.warning(
"""No title has been defined for property: \x1b[1m{0}\x1b[0m in group: \x1b[1m{1}\x1b[0m.
Title should be defined under path `schema.properties.{1}.properties.{0}.title`""".format(property, group)
)
pass
124 changes: 1 addition & 123 deletions cert_issuer/models.py → cert_issuer/models/verifiable_credential.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import re
from urllib.parse import urlparse
from abc import abstractmethod

from cert_schema import ContextUrls
from cert_issuer.config import ESTIMATE_NUM_INPUTS

# TODO: move the v3 checks to cert-schema
def validate_RFC3339_date (date):
Expand Down Expand Up @@ -141,123 +138,4 @@ def verify_presentation (certificate_metadata):
verify_credential(credential)
except:
raise ValueError('A Verifiable Presentation must contain valid verifiableCredential(s)')
pass

class BatchHandler(object):
def __init__(self, secret_manager, certificate_handler, merkle_tree, config):
self.certificate_handler = certificate_handler
self.secret_manager = secret_manager
self.merkle_tree = merkle_tree
self.config = config

@abstractmethod
def pre_batch_actions(self, config):
pass

@abstractmethod
def post_batch_actions(self, config):
pass

def set_certificates_in_batch(self, certificates_to_issue):
self.certificates_to_issue = certificates_to_issue


class CertificateHandler(object):
@abstractmethod
def validate_certificate(self, certificate_metadata):
validate_type(certificate_metadata['type'])
validate_context(certificate_metadata['@context'], certificate_metadata['type'])

if (certificate_metadata['type'][0] == 'VerifiableCredential'):
verify_credential(certificate_metadata)

if (certificate_metadata['type'][0] == 'VerifiablePresentation'):
verify_presentation(certificate_metadata)

pass

@abstractmethod
def sign_certificate(self, signer, certificate_metadata):
pass

@abstractmethod
def get_byte_array_to_issue(self, certificate_metadata):
pass

@abstractmethod
def add_proof(self, certificate_metadata, merkle_proof):
pass


class ServiceProviderConnector(object):
@abstractmethod
def get_balance(self, address):
pass

def broadcast_tx(self, tx):
pass


class Signer(object):
"""
Abstraction for a component that can sign.
"""

def __init__(self):
pass

@abstractmethod
def sign_message(self, wif, message_to_sign):
pass

@abstractmethod
def sign_transaction(self, wif, transaction_to_sign):
pass


class SecretManager(object):
def __init__(self, signer):
self.signer = signer
self.wif = None

@abstractmethod
def start(self):
pass

@abstractmethod
def stop(self):
pass

def sign_message(self, message_to_sign):
return self.signer.sign_message(self.wif, message_to_sign)

def sign_transaction(self, transaction_to_sign):
return self.signer.sign_transaction(self.wif, transaction_to_sign)


class TransactionHandler(object):
@abstractmethod
def ensure_balance(self):
pass

@abstractmethod
def issue_transaction(self, blockchain_bytes):
pass


class MockTransactionHandler(TransactionHandler):
def ensure_balance(self):
pass

def issue_transaction(self, op_return_bytes):
return 'This has not been issued on a blockchain and is for testing only'


class TransactionCreator(object):
@abstractmethod
def estimate_cost_for_certificate_batch(self, tx_cost_constants, num_inputs=ESTIMATE_NUM_INPUTS):
pass

@abstractmethod
def create_transaction(self, tx_cost_constants, issuing_address, inputs, op_return_value):
pass
pass
56 changes: 56 additions & 0 deletions tests/models/test_integration_verify_credential_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import unittest
import mock
import copy

from cert_issuer.certificate_handlers import CertificateBatchHandler, CertificateV3Handler
from cert_issuer.models import CertificateHandler

credential_example = {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1",
"https://www.blockcerts.org/schema/3.0/context.json"
],
"id": "urn:uuid:bbba8553-8ec1-445f-82c9-a57251dd731c",
"type": [
"VerifiableCredential",
"BlockcertsCredential"
],
"issuer": "https://raw.githubusercontent.com/AnthonyRonning/https-github.com-labnol-files/master/issuer-eth.json",
"issuanceDate": "2010-01-01T19:33:24Z",
"metadata": "{\"schema\":{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"type\":\"object\",\"properties\":{\"displayOrder\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"certificate\":{\"order\":[],\"type\":\"object\",\"properties\":{\"issuingInstitution\":{\"title\":\"Issuing Institution\",\"type\":\"string\",\"default\":\"Learning Machine Technologies, Inc.\"}}},\"recipient\":{}}},\"certificate\":{\"issuingInstitution\":\"Learning Machine Technologies, Inc.\"},\"recipient\":{},\"displayOrder\":{\"certificate.issuingInstitution\":1}}",
"credentialSubject": {
"id": "did:key:z6Mkq3L1jEDDZ5R7eT523FMLxC4k6MCpzqD7ff1CrkWpoJwM",
"alumniOf": {
"id": "did:example:c276e12ec21ebfeb1f712ebc6f1"
}
}
}

class TestIntegrationCredentialMetadata (unittest.TestCase):
def test_verify_metadata_invalid (self):
handler = CertificateBatchHandler(
secret_manager=mock.Mock(),
certificate_handler=MockCertificateV3Handler(credential_example),
merkle_tree=mock.Mock(),
config=mock.Mock()
)
handler.certificates_to_issue = {'metadata': mock.Mock()}

try:
handler.prepare_batch()
except Exception as e:
self.assertEqual(str(e), 'Certificate.metadata object does not match its provided schema')
return

assert False

class MockCertificateV3Handler(CertificateV3Handler):
def __init__(self, test_certificate):
self.test_certificate = test_certificate
print(self.test_certificate)
def _get_certificate_to_issue(self, data):
return self.test_certificate

if __name__ == '__main__':
unittest.main()
Loading

0 comments on commit 76c99cb

Please sign in to comment.