From 7e1e9e82843b026ae7afb06a4452f10e89514487 Mon Sep 17 00:00:00 2001 From: AnthonyRonning Date: Wed, 15 Jan 2020 09:19:29 -0600 Subject: [PATCH 01/48] Example for v3, update cert-schema, issuing v3 w/ old MerkleProof --- .../verifiable-credential.json | 17 +++++++++++++++++ requirements.txt | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 examples/data-testnet/unsigned_certificates/verifiable-credential.json diff --git a/examples/data-testnet/unsigned_certificates/verifiable-credential.json b/examples/data-testnet/unsigned_certificates/verifiable-credential.json new file mode 100644 index 00000000..d3178aea --- /dev/null +++ b/examples/data-testnet/unsigned_certificates/verifiable-credential.json @@ -0,0 +1,17 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.blockcerts.org/schema/3.0-alpha/context.json", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "urn:uuid:bbba8553-8ec1-445f-82c9-a57251dd731c", + "type": ["VerifiableCredential"], + "issuer": "did:example:23adb1f712ebc6f1c276eba4dfa", + "issuanceDate": "2010-01-01T19:73:24Z", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "alumniOf": { + "id": "did:example:c276e12ec21ebfeb1f712ebc6f1" + } + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e37c7d3d..b8ab2269 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cert-core>=2.1.9 -cert-schema>=2.1.5 +cert-schema>=3.0.0a1 chainpoint>=0.0.2 configargparse==0.12.0 glob2==0.6 From e7051ca3c4b251bfb1c0eb891ad41ad53052e713 Mon Sep 17 00:00:00 2001 From: AnthonyRonning Date: Tue, 21 Jan 2020 12:33:59 -0600 Subject: [PATCH 02/48] Integrated MerkleProof2019 into v3 issuing --- .../blockchain_handlers/bitcoin/__init__.py | 12 ++- .../blockchain_handlers/ethereum/__init__.py | 7 +- cert_issuer/certificate_handlers.py | 8 +- cert_issuer/helpers.py | 18 ++++ cert_issuer/merkle_tree_generator.py | 33 +++++-- cert_issuer/models.py | 3 +- requirements.txt | 3 +- tests/test_certificate_handler.py | 92 ++++++------------- tests/test_merkle_tree_generator.py | 79 +++++++++++++--- 9 files changed, 157 insertions(+), 98 deletions(-) diff --git a/cert_issuer/blockchain_handlers/bitcoin/__init__.py b/cert_issuer/blockchain_handlers/bitcoin/__init__.py index 4280bde3..8b34a87b 100644 --- a/cert_issuer/blockchain_handlers/bitcoin/__init__.py +++ b/cert_issuer/blockchain_handlers/bitcoin/__init__.py @@ -7,7 +7,7 @@ from cert_issuer.blockchain_handlers.bitcoin.connectors import BitcoinServiceProviderConnector, MockServiceProviderConnector from cert_issuer.blockchain_handlers.bitcoin.signer import BitcoinSigner from cert_issuer.blockchain_handlers.bitcoin.transaction_handlers import BitcoinTransactionHandler -from cert_issuer.certificate_handlers import CertificateBatchHandler, CertificateV2Handler, CertificateBatchWebHandler, CertificateWebV2Handler +from cert_issuer.certificate_handlers import CertificateBatchHandler, CertificateV3Handler, CertificateBatchWebHandler, CertificateWebV3Handler from cert_issuer.merkle_tree_generator import MerkleTreeGenerator from cert_issuer.models import MockTransactionHandler from cert_issuer.signer import FileSecretManager @@ -50,12 +50,14 @@ def instantiate_blockchain_handlers(app_config, file_mode=True): if file_mode: certificate_batch_handler = CertificateBatchHandler(secret_manager=secret_manager, - certificate_handler=CertificateV2Handler(), - merkle_tree=MerkleTreeGenerator()) + certificate_handler=CertificateV3Handler(), + merkle_tree=MerkleTreeGenerator(), + config=app_config) else: certificate_batch_handler = CertificateBatchWebHandler(secret_manager=secret_manager, - certificate_handler=CertificateWebV2Handler(), - merkle_tree=MerkleTreeGenerator()) + certificate_handler=CertificateWebV3Handler(), + merkle_tree=MerkleTreeGenerator(), + config=app_config) if chain == Chain.mockchain: transaction_handler = MockTransactionHandler() connector = MockServiceProviderConnector() diff --git a/cert_issuer/blockchain_handlers/ethereum/__init__.py b/cert_issuer/blockchain_handlers/ethereum/__init__.py index c2b37ca3..6fbac259 100644 --- a/cert_issuer/blockchain_handlers/ethereum/__init__.py +++ b/cert_issuer/blockchain_handlers/ethereum/__init__.py @@ -4,7 +4,7 @@ from cert_core import BlockchainType from cert_core import Chain, UnknownChainError -from cert_issuer.certificate_handlers import CertificateBatchHandler, CertificateV2Handler +from cert_issuer.certificate_handlers import CertificateBatchHandler, CertificateV3Handler from cert_issuer.blockchain_handlers.ethereum.connectors import EthereumServiceProviderConnector from cert_issuer.blockchain_handlers.ethereum.signer import EthereumSigner from cert_issuer.blockchain_handlers.ethereum.transaction_handlers import EthereumTransactionHandler @@ -54,8 +54,9 @@ def instantiate_blockchain_handlers(app_config): chain = app_config.chain secret_manager = initialize_signer(app_config) certificate_batch_handler = CertificateBatchHandler(secret_manager=secret_manager, - certificate_handler=CertificateV2Handler(), - merkle_tree=MerkleTreeGenerator()) + certificate_handler=CertificateV3Handler(), + merkle_tree=MerkleTreeGenerator(), + config=app_config) if chain == Chain.mockchain: transaction_handler = MockTransactionHandler() # ethereum chains diff --git a/cert_issuer/certificate_handlers.py b/cert_issuer/certificate_handlers.py index c1f050d0..33f5f8fb 100644 --- a/cert_issuer/certificate_handlers.py +++ b/cert_issuer/certificate_handlers.py @@ -9,7 +9,7 @@ from cert_issuer.signer import FinalizableSigner -class CertificateV2Handler(CertificateHandler): +class CertificateV3Handler(CertificateHandler): def get_byte_array_to_issue(self, certificate_metadata): certificate_json = self._get_certificate_to_issue(certificate_metadata) normalized = normalize_jsonld(certificate_json, detect_unmapped_fields=False) @@ -32,7 +32,7 @@ def _get_certificate_to_issue(self, certificate_metadata): certificate_json = json.load(unsigned_cert_file) return certificate_json -class CertificateWebV2Handler(CertificateHandler): +class CertificateWebV3Handler(CertificateHandler): def get_byte_array_to_issue(self, certificate_json): normalized = normalize_jsonld(certificate_json, detect_unmapped_fields=False) return normalized.encode('utf-8') @@ -49,7 +49,7 @@ def add_proof(self, certificate_json, merkle_proof): class CertificateBatchWebHandler(BatchHandler): def finish_batch(self, tx_id, chain): self.proof = [] - proof_generator = self.merkle_tree.get_proof_generator(tx_id, chain) + proof_generator = self.merkle_tree.get_proof_generator(tx_id, self.config.issuing_address, chain) for metadata in self.certificates_to_issue: proof = next(proof_generator) self.proof.append(self.certificate_handler.add_proof(metadata, proof)) @@ -120,7 +120,7 @@ def get_certificate_generator(self): yield data_to_issue def finish_batch(self, tx_id, chain): - proof_generator = self.merkle_tree.get_proof_generator(tx_id, chain) + proof_generator = self.merkle_tree.get_proof_generator(tx_id, self.config.issuing_address, chain) for _, metadata in self.certificates_to_issue.items(): proof = next(proof_generator) self.certificate_handler.add_proof(metadata, proof) diff --git a/cert_issuer/helpers.py b/cert_issuer/helpers.py index c953e7ef..91718b4b 100644 --- a/cert_issuer/helpers.py +++ b/cert_issuer/helpers.py @@ -100,3 +100,21 @@ def to_pycoin_chain(chain): return 'BTC' else: raise UnknownChainError(chain.name) + +def tx_to_blink(chain, tx_id): + blink = 'blink:' + if chain == Chain.bitcoin_regtest: + blink += 'btc:regtest:' + elif chain == Chain.bitcoin_testnet: + blink += 'btc:testnet:' + elif chain == Chain.bitcoin_mainnet: + blink += 'btc:mainnet:' + elif chain == Chain.ethereum_ropsten: + blink += 'eth:ropsten:' + elif chain == Chain.ethereum_mainnet: + blink += 'eth:mainnet:' + elif chain == Chain.mockchain: + blink += 'mocknet:' + else: + raise UnknownChainError(chain.name) + return blink + tx_id \ No newline at end of file diff --git a/cert_issuer/merkle_tree_generator.py b/cert_issuer/merkle_tree_generator.py index 6d10815e..9d10e9de 100644 --- a/cert_issuer/merkle_tree_generator.py +++ b/cert_issuer/merkle_tree_generator.py @@ -1,8 +1,12 @@ import hashlib +import logging +from datetime import datetime from cert_core import Chain from chainpoint.chainpoint import MerkleTools from pycoin.serialize import h2b +from lds_merkle_proof_2019.merkle_proof_2019 import MerkleProof2019 +from cert_issuer import helpers def hash_byte_array(data): @@ -40,7 +44,7 @@ def get_blockchain_data(self): merkle_root = self.tree.get_merkle_root() return h2b(ensure_string(merkle_root)) - def get_proof_generator(self, tx_id, chain=Chain.bitcoin_mainnet): + def get_proof_generator(self, tx_id, issuing_address, chain=Chain.bitcoin_mainnet): """ Returns a generator (1-time iterator) of proofs in insertion order. @@ -59,16 +63,25 @@ def get_proof_generator(self, tx_id, chain=Chain.bitcoin_mainnet): dict2[key] = ensure_string(value) proof2.append(dict2) target_hash = ensure_string(self.tree.get_leaf(index)) + mp2019 = MerkleProof2019() + merkle_json = { + "path": proof2, + "merkleRoot": root, + "targetHash": target_hash, + "anchors": [ + helpers.tx_to_blink(chain, tx_id) + ] + } + logging.info('merkle_json: %s', str(merkle_json)) + + proof_value = mp2019.encode(merkle_json) merkle_proof = { - "type": ['MerkleProof2017', 'Extension'], - "merkleRoot": root, - "targetHash": target_hash, - "proof": proof2, - "anchors": [{ - "sourceId": to_source_id(tx_id, chain), - "type": chain.blockchain_type.external_display_value, - "chain": chain.external_display_value - }]} + "type": "MerkleProof2019", + "created": datetime.now().isoformat(), + "proofValue": proof_value.decode('utf8'), + "proofPurpose": "assertionMethod", + "verificationMethod": "ecdsa-koblitz-pubkey:" + issuing_address + } yield merkle_proof diff --git a/cert_issuer/models.py b/cert_issuer/models.py index ce6279a6..90505d9e 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -3,10 +3,11 @@ from cert_issuer.config import ESTIMATE_NUM_INPUTS class BatchHandler(object): - def __init__(self, secret_manager, certificate_handler, merkle_tree): + 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): diff --git a/requirements.txt b/requirements.txt index b8ab2269..6edf6a0c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ pyld>=1.0.3 pysha3>=1.0.2 python-bitcoinlib>=0.10.1 tox>=3.0.0 -jsonschema<3.0.0 \ No newline at end of file +jsonschema<3.0.0 +lds-merkle-proof-2019>=0.0.1 \ No newline at end of file diff --git a/tests/test_certificate_handler.py b/tests/test_certificate_handler.py index 4975413c..49b19cae 100644 --- a/tests/test_certificate_handler.py +++ b/tests/test_certificate_handler.py @@ -6,63 +6,22 @@ from pycoin.serialize import b2h from mock import patch, mock_open -from cert_issuer.certificate_handlers import CertificateWebV2Handler, CertificateV2Handler, CertificateBatchHandler, CertificateHandler, CertificateBatchWebHandler +from cert_issuer.certificate_handlers import CertificateWebV3Handler, CertificateV3Handler, CertificateBatchHandler, CertificateHandler, CertificateBatchWebHandler from cert_issuer.merkle_tree_generator import MerkleTreeGenerator from cert_issuer import helpers +from cert_core import Chain from mock import ANY class TestCertificateHandler(unittest.TestCase): def _proof_helper(self, chain): proof = { - 'merkleRoot': '0932f1d2e98219f7d7452801e2b64ebd9e5c005539db12d9b1ddabe7834d9044', - 'type': ['MerkleProof2017', 'Extension'], - 'targetHash': ANY, - 'anchors': [ - { - 'sourceId': ANY, - 'type': chain.blockchain_type.external_display_value, - 'chain': chain.external_display_value - } - ], - 'proof': [ - {'right': 'd4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35'}, - {'right': '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'} - ], + 'type': 'MerkleProof2019', + 'created': ANY, + 'proofValue': ANY, + 'proofPurpose': 'assertionMethod', + 'verificationMethod': ANY } - - proof_1 = { - 'merkleRoot': '0932f1d2e98219f7d7452801e2b64ebd9e5c005539db12d9b1ddabe7834d9044', - 'type': ['MerkleProof2017', 'Extension'], - 'targetHash': ANY, - 'anchors': [ - { - 'sourceId': ANY, - 'type': chain.blockchain_type.external_display_value, - 'chain': chain.external_display_value - } - ], - 'proof': [ - {'left': '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b'}, - {'right': '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'} - ] - } - - proof_2 = { - 'merkleRoot': '0932f1d2e98219f7d7452801e2b64ebd9e5c005539db12d9b1ddabe7834d9044', - 'type': ['MerkleProof2017', 'Extension'], - 'targetHash': ANY, - 'anchors': [ - { - 'sourceId': ANY, - 'type': chain.blockchain_type.external_display_value, - 'chain': chain.external_display_value - } - ], - 'proof': [ - {'left': '4295f72eeb1e3507b8461e240e3b8d18c1e7bd2f1122b11fc9ec40a65894031a'} - ] - } - return proof, proof_1, proof_2 + return proof def _helper_mock_call(self, *args): helper_mock = mock.MagicMock() @@ -83,10 +42,14 @@ def _get_certificate_batch_web_handler(self): certificates_to_issue['2'] = mock.Mock() certificates_to_issue['3'] = mock.Mock() + config = mock.Mock() + config.issuing_address = "123" + handler = CertificateBatchWebHandler( secret_manager=secret_manager, certificate_handler=DummyCertificateHandler(), - merkle_tree=MerkleTreeGenerator()) + merkle_tree=MerkleTreeGenerator(), + config=config) return handler, certificates_to_issue @@ -97,10 +60,14 @@ def _get_certificate_batch_handler(self): certificates_to_issue['2'] = mock.Mock() certificates_to_issue['3'] = mock.Mock() + config = mock.Mock() + config.issuing_address = "123" + handler = CertificateBatchHandler( secret_manager=secret_manager, certificate_handler=DummyCertificateHandler(), - merkle_tree=MerkleTreeGenerator()) + merkle_tree=MerkleTreeGenerator(), + config=config) return handler, certificates_to_issue @@ -126,8 +93,8 @@ def test_batch_web_handler_finish_batch(self): certificate_batch_handler.set_certificates_in_batch(certificates_to_issue) result = certificate_batch_handler.prepare_batch() - chain = mock.Mock() - proof, proof_1, proof_2 = self._proof_helper(chain) + chain = Chain.bitcoin_mainnet + proof = self._proof_helper(chain) with patch.object(DummyCertificateHandler, 'add_proof', return_value= {"cert": "cert"} ) as mock_method: result = certificate_batch_handler.finish_batch( @@ -135,8 +102,6 @@ def test_batch_web_handler_finish_batch(self): ) self.assertEqual(certificate_batch_handler.proof, [{'cert': 'cert'}, {'cert': 'cert'}, {'cert': 'cert'}]) mock_method.assert_any_call(ANY, proof) - mock_method.assert_any_call(ANY, proof_1) - mock_method.assert_any_call(ANY, proof_2) def test_batch_handler_finish_batch(self): certificate_batch_handler, certificates_to_issue = self._get_certificate_batch_handler() @@ -144,8 +109,11 @@ def test_batch_handler_finish_batch(self): certificate_batch_handler.set_certificates_in_batch(certificates_to_issue) result = certificate_batch_handler.prepare_batch() - chain = mock.Mock() - proof, proof_1, proof_2 = self._proof_helper(chain) + chain = Chain.bitcoin_mainnet + proof = self._proof_helper(chain) + + config = mock.Mock() + config.issuing_address = "123" with patch.object(DummyCertificateHandler, 'add_proof') as mock_method: result = certificate_batch_handler.finish_batch( @@ -153,8 +121,6 @@ def test_batch_handler_finish_batch(self): ) mock_method.assert_any_call(ANY, proof) - mock_method.assert_any_call(ANY, proof_1) - mock_method.assert_any_call(ANY, proof_2) def test_pre_batch_actions(self): self.directory_count = 1 @@ -196,7 +162,7 @@ def test_pre_batch_actions_empty_directories(self): @mock.patch("builtins.open", create=True) def test_add_proof(self,mock_open): - handler = CertificateV2Handler() + handler = CertificateV3Handler() cert_to_issue = {'kek':'kek'} proof = {'a': 'merkel'} @@ -207,7 +173,7 @@ def test_add_proof(self,mock_open): metadata.blockchain_cert_file_name = 'file_path.nfo' with patch.object( - CertificateV2Handler, '_get_certificate_to_issue', return_value=cert_to_issue) as mock_method: + CertificateV3Handler, '_get_certificate_to_issue', return_value=cert_to_issue) as mock_method: handler.add_proof(metadata, proof) mock_open.assert_any_call('file_path.nfo','w') @@ -216,7 +182,7 @@ def test_add_proof(self,mock_open): assert file_call in call_strings def test_web_add_proof(self): - handler = CertificateWebV2Handler() + handler = CertificateWebV3Handler() proof = {'a': 'merkel'} chain = mock.Mock() certificate_json = {'kek': 'kek'} @@ -226,6 +192,8 @@ def test_web_add_proof(self): class DummyCertificateHandler(CertificateHandler): def __init__(self): + self.config = mock.Mock() + self.config.issuing_address = "123" self.counter = 0 def validate_certificate(self, certificate_metadata): diff --git a/tests/test_merkle_tree_generator.py b/tests/test_merkle_tree_generator.py index 20bd788d..1438f29c 100644 --- a/tests/test_merkle_tree_generator.py +++ b/tests/test_merkle_tree_generator.py @@ -4,6 +4,8 @@ from pycoin.serialize import b2h from cert_issuer.merkle_tree_generator import MerkleTreeGenerator +from cert_issuer import helpers +from lds_merkle_proof_2019.merkle_proof_2019 import MerkleProof2019 def get_test_data_generator(): @@ -29,36 +31,89 @@ def test_proofs_bitcoin_testnet(self): self.do_test_signature(Chain.bitcoin_testnet, 'bitcoinTestnet', 'BTCOpReturn') def test_proofs_bitcoin_regtest(self): - self.do_test_signature(Chain.bitcoin_regtest, 'bitcoinRegtest', 'BTCOpReturn') + ''' TODO merkle proof 2019 library with regtest ''' + # self.do_test_signature(Chain.bitcoin_regtest, 'bitcoinRegtest', 'BTCOpReturn') def test_proofs_mock(self): - self.do_test_signature(Chain.mockchain, 'mockchain', 'Mock') + ''' TODO merkle proof 2019 library with mocknet ''' + # self.do_test_signature(Chain.mockchain, 'mockchain', 'Mock') def do_test_signature(self, chain, display_chain, type): merkle_tree_generator = MerkleTreeGenerator() merkle_tree_generator.populate(get_test_data_generator()) _ = merkle_tree_generator.get_blockchain_data() gen = merkle_tree_generator.get_proof_generator( - '8087c03e7b7bc9ca7b355de9d9d8165cc5c76307f337f0deb8a204d002c8e582', chain) + '8087c03e7b7bc9ca7b355de9d9d8165cc5c76307f337f0deb8a204d002c8e582', '123', chain) p1 = next(gen) _ = next(gen) p3 = next(gen) - p1_expected = {'type': ['MerkleProof2017', 'Extension'], - 'merkleRoot': '0932f1d2e98219f7d7452801e2b64ebd9e5c005539db12d9b1ddabe7834d9044', - 'targetHash': '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', - 'proof': [{'right': 'd4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35'}, - {'right': '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'}], - 'anchors': [ - {'sourceId': '8087c03e7b7bc9ca7b355de9d9d8165cc5c76307f337f0deb8a204d002c8e582', + p1_expected_old = { + 'type': ['MerkleProof2017', 'Extension'], + 'merkleRoot': '0932f1d2e98219f7d7452801e2b64ebd9e5c005539db12d9b1ddabe7834d9044', + 'targetHash': '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', + 'proof': [ + {'right': 'd4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35'}, + {'right': '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'} + ], + 'anchors': [ + { + 'sourceId': '8087c03e7b7bc9ca7b355de9d9d8165cc5c76307f337f0deb8a204d002c8e582', 'type': type, - 'chain': display_chain}]} - p3_expected = {'type': ['MerkleProof2017', 'Extension'], + 'chain': display_chain + } + ] + } + + p1_json_proof = { + 'path': [ + {'right': 'd4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35'}, + {'right': '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'} + ], + 'merkleRoot': '0932f1d2e98219f7d7452801e2b64ebd9e5c005539db12d9b1ddabe7834d9044', + 'targetHash': '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', + 'anchors': [ + helpers.tx_to_blink(chain, '8087c03e7b7bc9ca7b355de9d9d8165cc5c76307f337f0deb8a204d002c8e582') + ] + } + mp2019 = MerkleProof2019() + proof_value = mp2019.encode(p1_json_proof) + + p1_expected = { + "type": "MerkleProof2019", + "created": p1['created'], + "proofValue": proof_value.decode('utf8'), + "proofPurpose": "assertionMethod", + "verificationMethod": "ecdsa-koblitz-pubkey:123" + } + + p3_expected_old = {'type': ['MerkleProof2017', 'Extension'], 'merkleRoot': '0932f1d2e98219f7d7452801e2b64ebd9e5c005539db12d9b1ddabe7834d9044', 'targetHash': '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce', 'proof': [{'left': '4295f72eeb1e3507b8461e240e3b8d18c1e7bd2f1122b11fc9ec40a65894031a'}], 'anchors': [{'sourceId': '8087c03e7b7bc9ca7b355de9d9d8165cc5c76307f337f0deb8a204d002c8e582', 'type': type, 'chain': display_chain}]} + p3_json_proof = { + 'path': [ + {'left': '4295f72eeb1e3507b8461e240e3b8d18c1e7bd2f1122b11fc9ec40a65894031a'} + ], + 'merkleRoot': '0932f1d2e98219f7d7452801e2b64ebd9e5c005539db12d9b1ddabe7834d9044', + 'targetHash': '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce', + 'anchors': [ + helpers.tx_to_blink(chain, '8087c03e7b7bc9ca7b355de9d9d8165cc5c76307f337f0deb8a204d002c8e582') + ] + } + mp2019 = MerkleProof2019() + proof_value = mp2019.encode(p3_json_proof) + + p3_expected = { + "type": "MerkleProof2019", + "created": p3['created'], + "proofValue": proof_value.decode('utf8'), + "proofPurpose": "assertionMethod", + "verificationMethod": "ecdsa-koblitz-pubkey:123" + } + self.assertEqual(p1, p1_expected) self.assertEqual(p3, p3_expected) From 2a3adb4d157924b1ea3124e24f0c893960663301 Mon Sep 17 00:00:00 2001 From: AnthonyRonning Date: Thu, 23 Jan 2020 11:46:08 -0600 Subject: [PATCH 03/48] #164 - Updated MerkleProof2019 to support mocknet & bitcoin regtest --- cert_issuer/__init__.py | 2 +- cert_issuer/merkle_tree_generator.py | 2 +- requirements.txt | 2 +- tests/test_merkle_tree_generator.py | 29 ++-------------------------- tox.ini | 1 + 5 files changed, 6 insertions(+), 30 deletions(-) diff --git a/cert_issuer/__init__.py b/cert_issuer/__init__.py index b03a62d0..6e648d3c 100644 --- a/cert_issuer/__init__.py +++ b/cert_issuer/__init__.py @@ -1 +1 @@ -__version__ = '2.0.20' +__version__ = '3.0.0a1' diff --git a/cert_issuer/merkle_tree_generator.py b/cert_issuer/merkle_tree_generator.py index 9d10e9de..08f02078 100644 --- a/cert_issuer/merkle_tree_generator.py +++ b/cert_issuer/merkle_tree_generator.py @@ -72,7 +72,7 @@ def get_proof_generator(self, tx_id, issuing_address, chain=Chain.bitcoin_mainne helpers.tx_to_blink(chain, tx_id) ] } - logging.info('merkle_json: %s', str(merkle_json)) + logging.debug('merkle_json: %s', str(merkle_json)) proof_value = mp2019.encode(merkle_json) merkle_proof = { diff --git a/requirements.txt b/requirements.txt index 6edf6a0c..a6cf0206 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ pysha3>=1.0.2 python-bitcoinlib>=0.10.1 tox>=3.0.0 jsonschema<3.0.0 -lds-merkle-proof-2019>=0.0.1 \ No newline at end of file +lds-merkle-proof-2019>=0.0.2 \ No newline at end of file diff --git a/tests/test_merkle_tree_generator.py b/tests/test_merkle_tree_generator.py index 1438f29c..88648abd 100644 --- a/tests/test_merkle_tree_generator.py +++ b/tests/test_merkle_tree_generator.py @@ -31,12 +31,10 @@ def test_proofs_bitcoin_testnet(self): self.do_test_signature(Chain.bitcoin_testnet, 'bitcoinTestnet', 'BTCOpReturn') def test_proofs_bitcoin_regtest(self): - ''' TODO merkle proof 2019 library with regtest ''' - # self.do_test_signature(Chain.bitcoin_regtest, 'bitcoinRegtest', 'BTCOpReturn') + self.do_test_signature(Chain.bitcoin_regtest, 'bitcoinRegtest', 'BTCOpReturn') def test_proofs_mock(self): - ''' TODO merkle proof 2019 library with mocknet ''' - # self.do_test_signature(Chain.mockchain, 'mockchain', 'Mock') + self.do_test_signature(Chain.mockchain, 'mockchain', 'Mock') def do_test_signature(self, chain, display_chain, type): merkle_tree_generator = MerkleTreeGenerator() @@ -47,22 +45,6 @@ def do_test_signature(self, chain, display_chain, type): p1 = next(gen) _ = next(gen) p3 = next(gen) - p1_expected_old = { - 'type': ['MerkleProof2017', 'Extension'], - 'merkleRoot': '0932f1d2e98219f7d7452801e2b64ebd9e5c005539db12d9b1ddabe7834d9044', - 'targetHash': '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', - 'proof': [ - {'right': 'd4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35'}, - {'right': '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'} - ], - 'anchors': [ - { - 'sourceId': '8087c03e7b7bc9ca7b355de9d9d8165cc5c76307f337f0deb8a204d002c8e582', - 'type': type, - 'chain': display_chain - } - ] - } p1_json_proof = { 'path': [ @@ -86,13 +68,6 @@ def do_test_signature(self, chain, display_chain, type): "verificationMethod": "ecdsa-koblitz-pubkey:123" } - p3_expected_old = {'type': ['MerkleProof2017', 'Extension'], - 'merkleRoot': '0932f1d2e98219f7d7452801e2b64ebd9e5c005539db12d9b1ddabe7834d9044', - 'targetHash': '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce', - 'proof': [{'left': '4295f72eeb1e3507b8461e240e3b8d18c1e7bd2f1122b11fc9ec40a65894031a'}], - 'anchors': [{'sourceId': '8087c03e7b7bc9ca7b355de9d9d8165cc5c76307f337f0deb8a204d002c8e582', - 'type': type, - 'chain': display_chain}]} p3_json_proof = { 'path': [ {'left': '4295f72eeb1e3507b8461e240e3b8d18c1e7bd2f1122b11fc9ec40a65894031a'} diff --git a/tox.ini b/tox.ini index f731e862..fa5e7bc5 100644 --- a/tox.ini +++ b/tox.ini @@ -11,5 +11,6 @@ envlist = py36 changedir=tests deps= pytest + -rrequirements.txt commands=py.test --basetemp={envtmpdir} {posargs} # substitute with tox' positional arguments From a40a7720ba8c36b8391b1224bff6ec3217e36b45 Mon Sep 17 00:00:00 2001 From: AnthonyRonning Date: Mon, 27 Jan 2020 10:02:45 -0600 Subject: [PATCH 04/48] Allowing issuer url/did's for verification method of ld merkle proof --- README.md | 3 +++ cert_issuer/__init__.py | 2 +- cert_issuer/certificate_handlers.py | 4 ++-- cert_issuer/config.py | 1 + cert_issuer/merkle_tree_generator.py | 6 +++--- conf_template.ini | 3 +++ tests/test_certificate_handler.py | 8 ++++---- tests/test_merkle_tree_generator.py | 6 +++--- 8 files changed, 20 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 34883f41..22b168c2 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,9 @@ Edit your conf.ini file (the config file for this application). ``` issuing_address = +# issuer URL / DID +verification_method = + chain= usb_name = diff --git a/cert_issuer/__init__.py b/cert_issuer/__init__.py index 6e648d3c..e3dc11f6 100644 --- a/cert_issuer/__init__.py +++ b/cert_issuer/__init__.py @@ -1 +1 @@ -__version__ = '3.0.0a1' +__version__ = '3.0.0a2' diff --git a/cert_issuer/certificate_handlers.py b/cert_issuer/certificate_handlers.py index 33f5f8fb..ad29b3b2 100644 --- a/cert_issuer/certificate_handlers.py +++ b/cert_issuer/certificate_handlers.py @@ -49,7 +49,7 @@ def add_proof(self, certificate_json, merkle_proof): class CertificateBatchWebHandler(BatchHandler): def finish_batch(self, tx_id, chain): self.proof = [] - proof_generator = self.merkle_tree.get_proof_generator(tx_id, self.config.issuing_address, chain) + proof_generator = self.merkle_tree.get_proof_generator(tx_id, self.config.verification_method, chain) for metadata in self.certificates_to_issue: proof = next(proof_generator) self.proof.append(self.certificate_handler.add_proof(metadata, proof)) @@ -120,7 +120,7 @@ def get_certificate_generator(self): yield data_to_issue def finish_batch(self, tx_id, chain): - proof_generator = self.merkle_tree.get_proof_generator(tx_id, self.config.issuing_address, chain) + proof_generator = self.merkle_tree.get_proof_generator(tx_id, self.config.verification_method, chain) for _, metadata in self.certificates_to_issue.items(): proof = next(proof_generator) self.certificate_handler.add_proof(metadata, proof) diff --git a/cert_issuer/config.py b/cert_issuer/config.py index bbdfbc2e..169be1f1 100644 --- a/cert_issuer/config.py +++ b/cert_issuer/config.py @@ -34,6 +34,7 @@ def add_arguments(p): p.add('-c', '--my-config', required=False, env_var='CONFIG_FILE', is_config_file=True, help='config file path') p.add_argument('--issuing_address', required=True, help='issuing address', env_var='ISSUING_ADDRESS') + p.add_argument('--verification_method', required=True, help='Verification method for the Linked Data Proof', env_var='VERIFICATION_METHOD') p.add_argument('--usb_name', required=True, help='usb path to key_file', env_var='USB_NAME') p.add_argument('--key_file', required=True, help='name of file on USB containing private key', env_var='KEY_FILE') diff --git a/cert_issuer/merkle_tree_generator.py b/cert_issuer/merkle_tree_generator.py index 08f02078..372fd94f 100644 --- a/cert_issuer/merkle_tree_generator.py +++ b/cert_issuer/merkle_tree_generator.py @@ -44,7 +44,7 @@ def get_blockchain_data(self): merkle_root = self.tree.get_merkle_root() return h2b(ensure_string(merkle_root)) - def get_proof_generator(self, tx_id, issuing_address, chain=Chain.bitcoin_mainnet): + def get_proof_generator(self, tx_id, verification_method, chain=Chain.bitcoin_mainnet): """ Returns a generator (1-time iterator) of proofs in insertion order. @@ -72,7 +72,7 @@ def get_proof_generator(self, tx_id, issuing_address, chain=Chain.bitcoin_mainne helpers.tx_to_blink(chain, tx_id) ] } - logging.debug('merkle_json: %s', str(merkle_json)) + logging.info('merkle_json: %s', str(merkle_json)) proof_value = mp2019.encode(merkle_json) merkle_proof = { @@ -80,7 +80,7 @@ def get_proof_generator(self, tx_id, issuing_address, chain=Chain.bitcoin_mainne "created": datetime.now().isoformat(), "proofValue": proof_value.decode('utf8'), "proofPurpose": "assertionMethod", - "verificationMethod": "ecdsa-koblitz-pubkey:" + issuing_address + "verificationMethod": verification_method } yield merkle_proof diff --git a/conf_template.ini b/conf_template.ini index 499ab702..07c6d1a7 100644 --- a/conf_template.ini +++ b/conf_template.ini @@ -1,5 +1,8 @@ issuing_address = +# Issuer URL / DID as the verification method +verification_method = + # put your unsigned certificates here for signing. Default is /data/unsigned_certificates unsigned_certificates_dir= # signed certificates are the output from the cert signing step; input to the cert issuing step. Default is /data/signed_certificates diff --git a/tests/test_certificate_handler.py b/tests/test_certificate_handler.py index 49b19cae..7c7e4923 100644 --- a/tests/test_certificate_handler.py +++ b/tests/test_certificate_handler.py @@ -43,7 +43,7 @@ def _get_certificate_batch_web_handler(self): certificates_to_issue['3'] = mock.Mock() config = mock.Mock() - config.issuing_address = "123" + config.issuing_address = "http://example.com" handler = CertificateBatchWebHandler( secret_manager=secret_manager, @@ -61,7 +61,7 @@ def _get_certificate_batch_handler(self): certificates_to_issue['3'] = mock.Mock() config = mock.Mock() - config.issuing_address = "123" + config.issuing_address = "http://example.com" handler = CertificateBatchHandler( secret_manager=secret_manager, @@ -113,7 +113,7 @@ def test_batch_handler_finish_batch(self): proof = self._proof_helper(chain) config = mock.Mock() - config.issuing_address = "123" + config.issuing_address = "http://example.com" with patch.object(DummyCertificateHandler, 'add_proof') as mock_method: result = certificate_batch_handler.finish_batch( @@ -193,7 +193,7 @@ def test_web_add_proof(self): class DummyCertificateHandler(CertificateHandler): def __init__(self): self.config = mock.Mock() - self.config.issuing_address = "123" + self.config.issuing_address = "http://example.com" self.counter = 0 def validate_certificate(self, certificate_metadata): diff --git a/tests/test_merkle_tree_generator.py b/tests/test_merkle_tree_generator.py index 88648abd..426a138c 100644 --- a/tests/test_merkle_tree_generator.py +++ b/tests/test_merkle_tree_generator.py @@ -41,7 +41,7 @@ def do_test_signature(self, chain, display_chain, type): merkle_tree_generator.populate(get_test_data_generator()) _ = merkle_tree_generator.get_blockchain_data() gen = merkle_tree_generator.get_proof_generator( - '8087c03e7b7bc9ca7b355de9d9d8165cc5c76307f337f0deb8a204d002c8e582', '123', chain) + '8087c03e7b7bc9ca7b355de9d9d8165cc5c76307f337f0deb8a204d002c8e582', 'http://example.com', chain) p1 = next(gen) _ = next(gen) p3 = next(gen) @@ -65,7 +65,7 @@ def do_test_signature(self, chain, display_chain, type): "created": p1['created'], "proofValue": proof_value.decode('utf8'), "proofPurpose": "assertionMethod", - "verificationMethod": "ecdsa-koblitz-pubkey:123" + "verificationMethod": "http://example.com" } p3_json_proof = { @@ -86,7 +86,7 @@ def do_test_signature(self, chain, display_chain, type): "created": p3['created'], "proofValue": proof_value.decode('utf8'), "proofPurpose": "assertionMethod", - "verificationMethod": "ecdsa-koblitz-pubkey:123" + "verificationMethod": "http://example.com" } self.assertEqual(p1, p1_expected) From b1c464742d7bd42002a3b558bd1b1166e886fd30 Mon Sep 17 00:00:00 2001 From: AnthonyRonning Date: Thu, 13 Feb 2020 10:47:59 -0600 Subject: [PATCH 05/48] bump cert-schema to 3.0.0a2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a6cf0206..ab4661c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cert-core>=2.1.9 -cert-schema>=3.0.0a1 +cert-schema>=3.0.0a2 chainpoint>=0.0.2 configargparse==0.12.0 glob2==0.6 From 830ee9d8ff2860976138dca8f2440c571e5d2170 Mon Sep 17 00:00:00 2001 From: AnthonyRonning Date: Wed, 19 Feb 2020 15:44:27 -0600 Subject: [PATCH 06/48] RI etherscan changes from v2 to v3 --- .travis.yml | 2 +- .../blockchain_handlers/ethereum/connectors.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 923b0188..beaa2d02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ sudo: false language: python python: - - "3.4" + - "3.6" install: pip install tox-travis script: tox diff --git a/cert_issuer/blockchain_handlers/ethereum/connectors.py b/cert_issuer/blockchain_handlers/ethereum/connectors.py index 1f75e84b..632289b2 100644 --- a/cert_issuer/blockchain_handlers/ethereum/connectors.py +++ b/cert_issuer/blockchain_handlers/ethereum/connectors.py @@ -95,12 +95,14 @@ def broadcast_tx(self, tx, api_token): broadcast_url = self.base_url + '?module=proxy&action=eth_sendRawTransaction' if api_token: - '&apikey=%s' % api_token + broadcast_url += '&apikey=%s' % api_token response = requests.post(broadcast_url, data={'hex': tx_hex}) if 'error' in response.json(): logging.error("Etherscan returned an error: %s", response.json()['error']) raise BroadcastError(response.json()['error']) if int(response.status_code) == 200: + if response.json().get('message', None) == 'NOTOK': + raise BroadcastError(response.json().get('result', None)) tx_id = response.json().get('result', None) logging.info("Transaction ID obtained from broadcast through Etherscan: %s", tx_id) return tx_id @@ -116,9 +118,11 @@ def get_balance(self, address, api_token): broadcast_url += '&address=%s' % address broadcast_url += '&tag=latest' if api_token: - '&apikey=%s' % api_token + broadcast_url += '&apikey=%s' % api_token response = requests.get(broadcast_url) if int(response.status_code) == 200: + if response.json().get('message', None) == 'NOTOK': + raise BroadcastError(response.json().get('result', None)) balance = int(response.json().get('result', None)) logging.info('Balance check succeeded: %s', response.json()) return balance @@ -133,10 +137,11 @@ def get_address_nonce(self, address, api_token): broadcast_url += '&address=%s' % address broadcast_url += '&tag=latest' if api_token: - '&apikey=%s' % api_token + broadcast_url += '&apikey=%s' % api_token response = requests.get(broadcast_url, ) if int(response.status_code) == 200: - # the int(res, 0) transforms the hex nonce to int + if response.json().get('message', None) == 'NOTOK': + raise BroadcastError(response.json().get('result', None)) nonce = int(response.json().get('result', None), 0) logging.info('Nonce check went correct: %s', response.json()) return nonce From 625b1f169fa52e3accd35eb3e56be644c72db190 Mon Sep 17 00:00:00 2001 From: AnthonyRonning Date: Wed, 19 Feb 2020 16:20:13 -0600 Subject: [PATCH 07/48] v3-alpha schema updates --- cert_issuer/__init__.py | 2 +- cert_issuer/certificate_handlers.py | 2 +- .../unsigned_certificates/verifiable-credential.json | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cert_issuer/__init__.py b/cert_issuer/__init__.py index e3dc11f6..7ccf291d 100644 --- a/cert_issuer/__init__.py +++ b/cert_issuer/__init__.py @@ -1 +1 @@ -__version__ = '3.0.0a2' +__version__ = '3.0.0a3' diff --git a/cert_issuer/certificate_handlers.py b/cert_issuer/certificate_handlers.py index ad29b3b2..3684ebf5 100644 --- a/cert_issuer/certificate_handlers.py +++ b/cert_issuer/certificate_handlers.py @@ -22,7 +22,7 @@ def add_proof(self, certificate_metadata, merkle_proof): :return: """ certificate_json = self._get_certificate_to_issue(certificate_metadata) - certificate_json['signature'] = merkle_proof + certificate_json['proof'] = merkle_proof with open(certificate_metadata.blockchain_cert_file_name, 'w') as out_file: out_file.write(json.dumps(certificate_json)) diff --git a/examples/data-testnet/unsigned_certificates/verifiable-credential.json b/examples/data-testnet/unsigned_certificates/verifiable-credential.json index d3178aea..37ab8c1d 100644 --- a/examples/data-testnet/unsigned_certificates/verifiable-credential.json +++ b/examples/data-testnet/unsigned_certificates/verifiable-credential.json @@ -5,13 +5,16 @@ "https://www.w3.org/2018/credentials/examples/v1" ], "id": "urn:uuid:bbba8553-8ec1-445f-82c9-a57251dd731c", - "type": ["VerifiableCredential"], + "type": [ + "VerifiableCredential", + "BlockcertsCredential" + ], "issuer": "did:example:23adb1f712ebc6f1c276eba4dfa", - "issuanceDate": "2010-01-01T19:73:24Z", + "issuanceDate": "2010-01-01T19:33:24Z", "credentialSubject": { "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", "alumniOf": { "id": "did:example:c276e12ec21ebfeb1f712ebc6f1" } } -} \ No newline at end of file +} From df2a44f8b36c8885a0da2df3080e05a006b7496e Mon Sep 17 00:00:00 2001 From: AnthonyRonning Date: Thu, 20 Feb 2020 10:55:34 -0600 Subject: [PATCH 08/48] Fix test using proof instead of signature for v3 --- tests/test_certificate_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_certificate_handler.py b/tests/test_certificate_handler.py index 7c7e4923..9a25689b 100644 --- a/tests/test_certificate_handler.py +++ b/tests/test_certificate_handler.py @@ -166,7 +166,7 @@ def test_add_proof(self,mock_open): cert_to_issue = {'kek':'kek'} proof = {'a': 'merkel'} - file_call = 'call().__enter__().write(\'{"kek": "kek", "signature": {"a": "merkel"}}\')' + file_call = 'call().__enter__().write(\'{"kek": "kek", "proof": {"a": "merkel"}}\')' chain = mock.Mock() metadata = mock.Mock() From c64f477e1000307a9d7db43c632d3d7c749f4383 Mon Sep 17 00:00:00 2001 From: AnthonyRonning Date: Thu, 20 Feb 2020 10:55:56 -0600 Subject: [PATCH 09/48] Bump v3-alpha schema to 3.0.0a3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ab4661c0..3275b404 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cert-core>=2.1.9 -cert-schema>=3.0.0a2 +cert-schema>=3.0.0a3 chainpoint>=0.0.2 configargparse==0.12.0 glob2==0.6 From 24a4f4f8ff202e51b77cc585b128ce9d862d49e2 Mon Sep 17 00:00:00 2001 From: AnthonyRonning Date: Wed, 11 Mar 2020 10:18:14 -0500 Subject: [PATCH 10/48] Bump v3-alpha schema to 3.0.0a4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3275b404..65ce1f19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cert-core>=2.1.9 -cert-schema>=3.0.0a3 +cert-schema>=3.0.0a4 chainpoint>=0.0.2 configargparse==0.12.0 glob2==0.6 From 3fb247703d5ce35d11c632e46bcad33e097c39ef Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Fri, 24 Jul 2020 16:08:38 +0200 Subject: [PATCH 11/48] fix(Issuance): settle pyld to v1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 65ce1f19..3c91225c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ glob2==0.6 mock==2.0.0 requests[security]>=2.18.4 pycoin==0.80 -pyld>=1.0.3 +pyld==1.0.5 pysha3>=1.0.2 python-bitcoinlib>=0.10.1 tox>=3.0.0 From cd793cbc3b787c10856acb5c52b1dcfb53e08ec3 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Fri, 2 Oct 2020 11:24:57 +0200 Subject: [PATCH 12/48] VC testing scaffholding --- .gitignore | 1 + index.js | 13 +++++++ middlewares/verify.js | 83 +++++++++++++++++++++++++++++++++++++++++++ node-wrapper-issuer | 1 + 4 files changed, 98 insertions(+) create mode 100644 index.js create mode 100644 middlewares/verify.js create mode 160000 node-wrapper-issuer diff --git a/.gitignore b/.gitignore index 14ef34ad..3d71df11 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ data/work/* data*/* dist/* build/* +node_modules/* requirements.txt .idea/* diff --git a/index.js b/index.js new file mode 100644 index 00000000..ca821a85 --- /dev/null +++ b/index.js @@ -0,0 +1,13 @@ +const express = require('express'); +const bodyParser = require('body-parser'); +const verify = require('./middlewares/verify'); +const server = express(); + +server.use(bodyParser.json({limit: '5mb'})); +const port = 3000; + +server.get('/', (req, res) => res.send('Hello World!')); + +server.post('/verify', verify); + +server.listen(port, () => console.log(`Example app listening at http://localhost:${port}`)); diff --git a/middlewares/verify.js b/middlewares/verify.js new file mode 100644 index 00000000..b5adfef3 --- /dev/null +++ b/middlewares/verify.js @@ -0,0 +1,83 @@ +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +function saveFileToUnsignedCertificates (data) { + const targetPath = path.join(__dirname, '..', 'data/unsigned_certificates', 'sample.json'); + fs.writeFile(targetPath, JSON.stringify(data), (err) => { + if (err) { + throw err; + } + console.log('The file has been saved!'); + }); +} + +async function getGeneratedCertificate () { + const targetPath = path.join(__dirname, '..', 'data/blockchain_certificates', 'sample.json'); + return new Promise((resolve, reject) => { + fs.readFile(targetPath, 'utf8', (err, data) => { + if (err) { + reject(err); + } + resolve(JSON.parse(data)); + }); + }); +} + +function verify (req, res) { + const cert = req.body.certificate; + console.log('now processing', cert); + + saveFileToUnsignedCertificates(cert); + + return new Promise((resolve, reject) => { + let stdout = []; + let stderr = []; + const verificationProcess = spawn('python3', ['cert_issuer', '-c', 'conf.ini']); + verificationProcess.stdout.pipe(process.stdout); + + verificationProcess.on('error', err => reject(new Error(err))); + verificationProcess.stdout.on('error', err => reject(new Error(err))); + verificationProcess.stderr.on('error', err => reject(new Error(err))); + verificationProcess.stdin.on('error', err => reject(new Error(err))); + + verificationProcess.stdout.on('data', data => stdout.push(data)); + verificationProcess.stderr.on('data', data => stderr.push(data)); + + verificationProcess.stdin.end(''); + + verificationProcess.on('close', async code => { + stdout = stdout.join('').trim(); + stderr = stderr.join('').trim(); + + if (code === 0) { + console.log(stdout, stderr); + console.log('success'); + const certificate = await getGeneratedCertificate(); + console.log(certificate); + res.send({ + success: true, + certificate + }); + return resolve({ successResolve: true }); + } + + let error = new Error(`command exited with code: ${code}\n\n ${stdout}\n\n ${stderr}`); + + // emulate actual Child Process Errors + error.path = 'python3'; + error.syscall = 'spawn python3'; + error.spawnargs = ['cert_issuer', '-c', 'conf.ini']; + + res.send({ + success: false, + error, + stderr + }); + return reject(error); + }) + }); +} + + +module.exports = verify; diff --git a/node-wrapper-issuer b/node-wrapper-issuer new file mode 160000 index 00000000..503c60b5 --- /dev/null +++ b/node-wrapper-issuer @@ -0,0 +1 @@ +Subproject commit 503c60b5f8dea4d6ecd440e689af1fdb3a7bdf52 From ce99887bcd0582ce86f7da0034552c4dfbfcd861 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Tue, 6 Oct 2020 14:24:20 +0200 Subject: [PATCH 13/48] feat(VC): validate type property --- cert_issuer/certificate_handlers.py | 3 ++- cert_issuer/models.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cert_issuer/certificate_handlers.py b/cert_issuer/certificate_handlers.py index 3684ebf5..11f14344 100644 --- a/cert_issuer/certificate_handlers.py +++ b/cert_issuer/certificate_handlers.py @@ -99,7 +99,8 @@ def prepare_batch(self): # validate batch for _, metadata in self.certificates_to_issue.items(): - self.certificate_handler.validate_certificate(metadata) + certificate_json = self.certificate_handler._get_certificate_to_issue(metadata) + self.certificate_handler.validate_certificate(certificate_json) # sign batch with FinalizableSigner(self.secret_manager) as signer: diff --git a/cert_issuer/models.py b/cert_issuer/models.py index 90505d9e..2814515d 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -2,6 +2,16 @@ from cert_issuer.config import ESTIMATE_NUM_INPUTS +def validate_type (certificate_type): + compulsory_types = ['VerifiableCredential', 'VerifiablePresentation'] + if not isinstance(certificate_type, list): + raise ValueError('`type` property should be an array') + if isinstance(certificate_type, list) and len(certificate_type) <= 1: + raise ValueError('`type` property should be an array with at least 2 values') + if isinstance(certificate_type, list) and certificate_metadata['type'][0] not in compulsory_types: + raise ValueError('`type` property first value should be either VerifiableCredential or VerifiablePresentation') + pass + class BatchHandler(object): def __init__(self, secret_manager, certificate_handler, merkle_tree, config): self.certificate_handler = certificate_handler @@ -24,6 +34,7 @@ def set_certificates_in_batch(self, certificates_to_issue): class CertificateHandler(object): @abstractmethod def validate_certificate(self, certificate_metadata): + validate_type(certificate_metadata['type']) pass @abstractmethod From 0d5a8424ae3cf983033b36644deaee4b224cfaab Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Tue, 6 Oct 2020 15:30:16 +0200 Subject: [PATCH 14/48] feat(VC): validate credentialSubject property --- cert_issuer/models.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cert_issuer/models.py b/cert_issuer/models.py index 2814515d..1f3055d3 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -8,10 +8,13 @@ def validate_type (certificate_type): raise ValueError('`type` property should be an array') if isinstance(certificate_type, list) and len(certificate_type) <= 1: raise ValueError('`type` property should be an array with at least 2 values') - if isinstance(certificate_type, list) and certificate_metadata['type'][0] not in compulsory_types: + if isinstance(certificate_type, list) and certificate_type[0] not in compulsory_types: raise ValueError('`type` property first value should be either VerifiableCredential or VerifiablePresentation') pass +def validate_credential_subject (credential_subject): + pass + class BatchHandler(object): def __init__(self, secret_manager, certificate_handler, merkle_tree, config): self.certificate_handler = certificate_handler @@ -35,6 +38,11 @@ class CertificateHandler(object): @abstractmethod def validate_certificate(self, certificate_metadata): validate_type(certificate_metadata['type']) + try: + # if undefined will throw KeyError + validate_credential_subject(certificate_metadata['credentialSubject']) + except: + raise ValueError('`credentialSubject property must be defined`') pass @abstractmethod From 07617733b7da61eb69bd25ef01a70601e2f52cbd Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Tue, 6 Oct 2020 17:30:34 +0200 Subject: [PATCH 15/48] feat(VC): validate issuer property --- cert_issuer/models.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cert_issuer/models.py b/cert_issuer/models.py index 1f3055d3..a722af5f 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -15,6 +15,11 @@ def validate_type (certificate_type): def validate_credential_subject (credential_subject): pass +def validate_issuer (certificate_issuer): + if not isinstance(certificate_issuer, str): + raise ValueError('`issuer` property must be a string') + pass + class BatchHandler(object): def __init__(self, secret_manager, certificate_handler, merkle_tree, config): self.certificate_handler = certificate_handler @@ -38,11 +43,21 @@ class CertificateHandler(object): @abstractmethod def validate_certificate(self, certificate_metadata): validate_type(certificate_metadata['type']) + try: # if undefined will throw KeyError validate_credential_subject(certificate_metadata['credentialSubject']) except: raise ValueError('`credentialSubject property must be defined`') + + try: + # if undefined will throw KeyError + validate_issuer(certificate_metadata['issuer']) + except KeyError: + raise ValueError('`issuer property must be defined`') + except ValueError as err: + raise ValueError(err) + pass @abstractmethod From 998fdbef00de1ba25d91561b3ba09269055bd391 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 7 Oct 2020 14:41:46 +0200 Subject: [PATCH 16/48] feat(VC): validate issuanceDate property --- cert_issuer/models.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cert_issuer/models.py b/cert_issuer/models.py index a722af5f..87f1a05a 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -1,3 +1,4 @@ +import re from abc import abstractmethod from cert_issuer.config import ESTIMATE_NUM_INPUTS @@ -20,6 +21,17 @@ def validate_issuer (certificate_issuer): raise ValueError('`issuer` property must be a string') pass +def validate_issuance_date (certificate_issuance_date): + has_error = False + if not isinstance(certificate_issuance_date, str): + raise ValueError('`issuance_date` property must be a RFC3339 valid string') + + is_valid_RFC3339_date = re.match('^[1-9]\d{3}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}[Zz]$', certificate_issuance_date) + if not is_valid_RFC3339_date: + raise ValueError('`issuance_date` property must be a RFC3339 valid string') + + pass + class BatchHandler(object): def __init__(self, secret_manager, certificate_handler, merkle_tree, config): self.certificate_handler = certificate_handler @@ -58,6 +70,14 @@ def validate_certificate(self, certificate_metadata): except ValueError as err: raise ValueError(err) + try: + # if undefined will throw KeyError + validate_issuance_date(certificate_metadata['issuanceDate']) + except KeyError: + raise ValueError('`issuance_date property must be defined`') + except ValueError as err: + raise ValueError(err) + pass @abstractmethod From bf26234fbefc92937278d50ce3a92439c1695326 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 7 Oct 2020 15:12:27 +0200 Subject: [PATCH 17/48] feat(VC): validate expirationDate property --- cert_issuer/models.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/cert_issuer/models.py b/cert_issuer/models.py index 87f1a05a..6f64539d 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -21,15 +21,24 @@ def validate_issuer (certificate_issuer): raise ValueError('`issuer` property must be a string') pass -def validate_issuance_date (certificate_issuance_date): - has_error = False - if not isinstance(certificate_issuance_date, str): - raise ValueError('`issuance_date` property must be a RFC3339 valid string') +def validate_RFC3339_date (date): + return re.match('^[1-9]\d{3}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}[Zz]$', date) + +def validate_date_RFC3339_string_format (date, property_name): + error_message = '{} property must be a valid RFC3339 string'.format(property_name) + if not isinstance(date, str): + raise ValueError(error_message) + + if not validate_RFC3339_date(date): + raise ValueError(error_message) + pass - is_valid_RFC3339_date = re.match('^[1-9]\d{3}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}[Zz]$', certificate_issuance_date) - if not is_valid_RFC3339_date: - raise ValueError('`issuance_date` property must be a RFC3339 valid string') +def validate_issuance_date (certificate_issuance_date): + validate_date_RFC3339_string_format(certificate_issuance_date, 'issuanceDate') + pass +def validate_expiration_date (certificate_expiration_date): + validate_date_RFC3339_string_format(certificate_expiration_date, 'expirationDate') pass class BatchHandler(object): @@ -78,6 +87,14 @@ def validate_certificate(self, certificate_metadata): except ValueError as err: raise ValueError(err) + try: + # if undefined will throw KeyError + validate_expiration_date(certificate_metadata['expirationDate']) + except KeyError: + pass + except ValueError as err: + raise ValueError(err) + pass @abstractmethod From e5bc0c9505fb46f0276c102b169b0faecfca0d9f Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 7 Oct 2020 17:52:26 +0200 Subject: [PATCH 18/48] feat(VC): validate credentialStatus property --- cert_issuer/models.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/cert_issuer/models.py b/cert_issuer/models.py index 6f64539d..3814d1d3 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -1,8 +1,18 @@ import re +from urllib.parse import urlparse from abc import abstractmethod from cert_issuer.config import ESTIMATE_NUM_INPUTS +def validate_RFC3339_date (date): + return re.match('^[1-9]\d{3}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}[Zz]$', date) + +def validate_url (url): + parsed_url = urlparse(url) + if parsed_url.path.__contains__(' ') or parsed_url.netloc.__contains__(' '): + raise ValueError('Invalid URL') + pass + def validate_type (certificate_type): compulsory_types = ['VerifiableCredential', 'VerifiablePresentation'] if not isinstance(certificate_type, list): @@ -21,9 +31,6 @@ def validate_issuer (certificate_issuer): raise ValueError('`issuer` property must be a string') pass -def validate_RFC3339_date (date): - return re.match('^[1-9]\d{3}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}[Zz]$', date) - def validate_date_RFC3339_string_format (date, property_name): error_message = '{} property must be a valid RFC3339 string'.format(property_name) if not isinstance(date, str): @@ -41,6 +48,22 @@ def validate_expiration_date (certificate_expiration_date): validate_date_RFC3339_string_format(certificate_expiration_date, 'expirationDate') pass +def validate_credential_status (certificate_credential_status): + try: + validate_url(certificate_credential_status['id']) + except KeyError: + raise ValueError('credentialStatus.id must be defined') + except ValueError: + raise ValueError('credentialStatus.id must be a valid URL') + + try: + isinstance(certificate_credential_status['type'], str) + except KeyError: + raise ValueError('credentialStatus.type must be defined') + except: + raise ValueError('credentialStatus.type must be a string') + pass + class BatchHandler(object): def __init__(self, secret_manager, certificate_handler, merkle_tree, config): self.certificate_handler = certificate_handler @@ -91,6 +114,16 @@ def validate_certificate(self, certificate_metadata): # if undefined will throw KeyError validate_expiration_date(certificate_metadata['expirationDate']) except KeyError: + # optional property + pass + except ValueError as err: + raise ValueError(err) + + try: + # if undefined will throw KeyError + validate_credential_status(certificate_metadata['credentialStatus']) + except KeyError: + # optional property pass except ValueError as err: raise ValueError(err) From 222233d799e1ce3d1f42684e5b8ae2371b3c1088 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Thu, 8 Oct 2020 16:02:19 +0200 Subject: [PATCH 19/48] feat(VC): validate verifiable presentations --- cert_issuer/models.py | 105 ++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 44 deletions(-) diff --git a/cert_issuer/models.py b/cert_issuer/models.py index 3814d1d3..41dc3a9f 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -16,11 +16,11 @@ def validate_url (url): def validate_type (certificate_type): compulsory_types = ['VerifiableCredential', 'VerifiablePresentation'] if not isinstance(certificate_type, list): - raise ValueError('`type` property should be an array') - if isinstance(certificate_type, list) and len(certificate_type) <= 1: - raise ValueError('`type` property should be an array with at least 2 values') - if isinstance(certificate_type, list) and certificate_type[0] not in compulsory_types: - raise ValueError('`type` property first value should be either VerifiableCredential or VerifiablePresentation') + raise ValueError('`type` property must be an array') + + contains_compulsory_types = list(set(compulsory_types) & set(certificate_type)) + if len(certificate_type) == 0 or len(contains_compulsory_types) == 0: + raise ValueError('`type` property must be an array with at least `VerifiableCredential` or `VerifiablePresentation` value') pass def validate_credential_subject (credential_subject): @@ -64,6 +64,57 @@ def validate_credential_status (certificate_credential_status): raise ValueError('credentialStatus.type must be a string') pass +def verify_credential(certificate_metadata): + try: + # if undefined will throw KeyError + validate_credential_subject(certificate_metadata['credentialSubject']) + except: + raise ValueError('`credentialSubject property must be defined`') + + try: + # if undefined will throw KeyError + validate_issuer(certificate_metadata['issuer']) + except KeyError: + raise ValueError('`issuer property must be defined`') + except ValueError as err: + raise ValueError(err) + + try: + # if undefined will throw KeyError + validate_issuance_date(certificate_metadata['issuanceDate']) + except KeyError: + raise ValueError('`issuance_date property must be defined`') + except ValueError as err: + raise ValueError(err) + + try: + # if undefined will throw KeyError + validate_expiration_date(certificate_metadata['expirationDate']) + except KeyError: + # optional property + pass + except ValueError as err: + raise ValueError(err) + + try: + # if undefined will throw KeyError + validate_credential_status(certificate_metadata['credentialStatus']) + except KeyError: + # optional property + pass + except ValueError as err: + raise ValueError(err) + + pass + +def verify_presentation (certificate_metadata): + try: + for credential in certificate_metadata['verifiableCredential']: + 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 @@ -88,45 +139,11 @@ class CertificateHandler(object): def validate_certificate(self, certificate_metadata): validate_type(certificate_metadata['type']) - try: - # if undefined will throw KeyError - validate_credential_subject(certificate_metadata['credentialSubject']) - except: - raise ValueError('`credentialSubject property must be defined`') - - try: - # if undefined will throw KeyError - validate_issuer(certificate_metadata['issuer']) - except KeyError: - raise ValueError('`issuer property must be defined`') - except ValueError as err: - raise ValueError(err) - - try: - # if undefined will throw KeyError - validate_issuance_date(certificate_metadata['issuanceDate']) - except KeyError: - raise ValueError('`issuance_date property must be defined`') - except ValueError as err: - raise ValueError(err) - - try: - # if undefined will throw KeyError - validate_expiration_date(certificate_metadata['expirationDate']) - except KeyError: - # optional property - pass - except ValueError as err: - raise ValueError(err) - - try: - # if undefined will throw KeyError - validate_credential_status(certificate_metadata['credentialStatus']) - except KeyError: - # optional property - pass - except ValueError as err: - raise ValueError(err) + if (certificate_metadata['type'][0] == 'VerifiableCredential'): + verify_credential(certificate_metadata) + + if (certificate_metadata['type'][0] == 'VerifiablePresentation'): + verify_presentation(certificate_metadata) pass From a38cb9a0f2652e75d94bbd52c4b0d50f185ff054 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Thu, 8 Oct 2020 17:31:46 +0200 Subject: [PATCH 20/48] feat(VC): validate @context property --- cert_issuer/models.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cert_issuer/models.py b/cert_issuer/models.py index 41dc3a9f..6471ac5d 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -23,6 +23,18 @@ def validate_type (certificate_type): raise ValueError('`type` property must be an array with at least `VerifiableCredential` or `VerifiablePresentation` value') pass +def validate_context (context, type): + vc_context_url = 'https://www.w3.org/2018/credentials/v1' + + if not isinstance(context, list): + raise ValueError('@context property must be an array') + if context[0] != vc_context_url: + raise ValueError('First @context declared must be {}, was given {}'.format(vc_context_url, context[0])) + if len(type) > 1 and len(context) == 1: + raise ValueError('A more specific type: {}, was detected, yet no context seems provided for that type'.format(type[1])) + + pass + def validate_credential_subject (credential_subject): pass @@ -138,6 +150,7 @@ 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) From 234df381fadd66afc6ce9265215c3ab232c0cc2b Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Thu, 8 Oct 2020 18:04:25 +0200 Subject: [PATCH 21/48] feat(VC): force issuer property to be a URL --- cert_issuer/models.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cert_issuer/models.py b/cert_issuer/models.py index 6471ac5d..1c9dc15b 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -9,8 +9,12 @@ def validate_RFC3339_date (date): def validate_url (url): parsed_url = urlparse(url) - if parsed_url.path.__contains__(' ') or parsed_url.netloc.__contains__(' '): - raise ValueError('Invalid URL') + is_valid_url = (not (parsed_url.path.__contains__(' ') + or parsed_url.netloc.__contains__(' ')) + and url.__contains__(':')) + + if not is_valid_url: + raise ValueError('Invalid URL: {}'.format(url)) pass def validate_type (certificate_type): @@ -39,8 +43,10 @@ def validate_credential_subject (credential_subject): pass def validate_issuer (certificate_issuer): - if not isinstance(certificate_issuer, str): - raise ValueError('`issuer` property must be a string') + try: + validate_url(certificate_issuer) + except: + raise ValueError('`issuer` property must be a URL string') pass def validate_date_RFC3339_string_format (date, property_name): From 7703e50525f019c54a7caa590a1d1e312b91ecd5 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Fri, 9 Oct 2020 18:23:05 +0200 Subject: [PATCH 22/48] test scaffholding to run local code --- middlewares/verify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middlewares/verify.js b/middlewares/verify.js index b5adfef3..aee100e5 100644 --- a/middlewares/verify.js +++ b/middlewares/verify.js @@ -33,7 +33,7 @@ function verify (req, res) { return new Promise((resolve, reject) => { let stdout = []; let stderr = []; - const verificationProcess = spawn('python3', ['cert_issuer', '-c', 'conf.ini']); + const verificationProcess = spawn('python3', ['cert_issuer/__main__.py', '-c', 'conf.ini']); verificationProcess.stdout.pipe(process.stdout); verificationProcess.on('error', err => reject(new Error(err))); From ad5e454af9cdf6b2c85f63c9ce9fd1b803804bd7 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Tue, 13 Oct 2020 15:33:01 +0200 Subject: [PATCH 23/48] chore(VC): remove js test scaffholding --- index.js | 13 ------- middlewares/verify.js | 83 ------------------------------------------- node-wrapper-issuer | 1 - 3 files changed, 97 deletions(-) delete mode 100644 index.js delete mode 100644 middlewares/verify.js delete mode 160000 node-wrapper-issuer diff --git a/index.js b/index.js deleted file mode 100644 index ca821a85..00000000 --- a/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const express = require('express'); -const bodyParser = require('body-parser'); -const verify = require('./middlewares/verify'); -const server = express(); - -server.use(bodyParser.json({limit: '5mb'})); -const port = 3000; - -server.get('/', (req, res) => res.send('Hello World!')); - -server.post('/verify', verify); - -server.listen(port, () => console.log(`Example app listening at http://localhost:${port}`)); diff --git a/middlewares/verify.js b/middlewares/verify.js deleted file mode 100644 index aee100e5..00000000 --- a/middlewares/verify.js +++ /dev/null @@ -1,83 +0,0 @@ -const { spawn } = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -function saveFileToUnsignedCertificates (data) { - const targetPath = path.join(__dirname, '..', 'data/unsigned_certificates', 'sample.json'); - fs.writeFile(targetPath, JSON.stringify(data), (err) => { - if (err) { - throw err; - } - console.log('The file has been saved!'); - }); -} - -async function getGeneratedCertificate () { - const targetPath = path.join(__dirname, '..', 'data/blockchain_certificates', 'sample.json'); - return new Promise((resolve, reject) => { - fs.readFile(targetPath, 'utf8', (err, data) => { - if (err) { - reject(err); - } - resolve(JSON.parse(data)); - }); - }); -} - -function verify (req, res) { - const cert = req.body.certificate; - console.log('now processing', cert); - - saveFileToUnsignedCertificates(cert); - - return new Promise((resolve, reject) => { - let stdout = []; - let stderr = []; - const verificationProcess = spawn('python3', ['cert_issuer/__main__.py', '-c', 'conf.ini']); - verificationProcess.stdout.pipe(process.stdout); - - verificationProcess.on('error', err => reject(new Error(err))); - verificationProcess.stdout.on('error', err => reject(new Error(err))); - verificationProcess.stderr.on('error', err => reject(new Error(err))); - verificationProcess.stdin.on('error', err => reject(new Error(err))); - - verificationProcess.stdout.on('data', data => stdout.push(data)); - verificationProcess.stderr.on('data', data => stderr.push(data)); - - verificationProcess.stdin.end(''); - - verificationProcess.on('close', async code => { - stdout = stdout.join('').trim(); - stderr = stderr.join('').trim(); - - if (code === 0) { - console.log(stdout, stderr); - console.log('success'); - const certificate = await getGeneratedCertificate(); - console.log(certificate); - res.send({ - success: true, - certificate - }); - return resolve({ successResolve: true }); - } - - let error = new Error(`command exited with code: ${code}\n\n ${stdout}\n\n ${stderr}`); - - // emulate actual Child Process Errors - error.path = 'python3'; - error.syscall = 'spawn python3'; - error.spawnargs = ['cert_issuer', '-c', 'conf.ini']; - - res.send({ - success: false, - error, - stderr - }); - return reject(error); - }) - }); -} - - -module.exports = verify; diff --git a/node-wrapper-issuer b/node-wrapper-issuer deleted file mode 160000 index 503c60b5..00000000 --- a/node-wrapper-issuer +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 503c60b5f8dea4d6ecd440e689af1fdb3a7bdf52 From 11c727b1e71416c1dc8fe30bf63573553b1117d0 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Tue, 13 Oct 2020 16:01:36 +0200 Subject: [PATCH 24/48] chore(VC): bump version --- cert_issuer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cert_issuer/__init__.py b/cert_issuer/__init__.py index 7ccf291d..6f3711e7 100644 --- a/cert_issuer/__init__.py +++ b/cert_issuer/__init__.py @@ -1 +1 @@ -__version__ = '3.0.0a3' +__version__ = '3.0.0a4' From 9ec7160815263ae578353bd8c7c9f4eabe4840c0 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 21 Oct 2020 15:02:20 +0200 Subject: [PATCH 25/48] tests(V3): fix tests failure --- tests/test_certificate_handler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_certificate_handler.py b/tests/test_certificate_handler.py index 9a25689b..1a82ac9c 100644 --- a/tests/test_certificate_handler.py +++ b/tests/test_certificate_handler.py @@ -196,6 +196,9 @@ def __init__(self): self.config.issuing_address = "http://example.com" self.counter = 0 + def _get_certificate_to_issue (self, certificate_metadata): + pass + def validate_certificate(self, certificate_metadata): pass From 0004099fd716d17e16bb4918f5a25c7b63e7d9c7 Mon Sep 17 00:00:00 2001 From: Lucas Parker Date: Fri, 20 Nov 2020 14:55:59 -0800 Subject: [PATCH 26/48] Revving to 3.0.0b1 --- cert_issuer/__init__.py | 2 +- requirements.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cert_issuer/__init__.py b/cert_issuer/__init__.py index 6f3711e7..c8ce6e55 100644 --- a/cert_issuer/__init__.py +++ b/cert_issuer/__init__.py @@ -1 +1 @@ -__version__ = '3.0.0a4' +__version__ = '3.0.0b1' diff --git a/requirements.txt b/requirements.txt index 3c91225c..b9202bda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -cert-core>=2.1.9 -cert-schema>=3.0.0a4 +cert-core>=3.0.0b1 +cert-schema>=3.0.0b1 chainpoint>=0.0.2 configargparse==0.12.0 glob2==0.6 @@ -11,4 +11,4 @@ pysha3>=1.0.2 python-bitcoinlib>=0.10.1 tox>=3.0.0 jsonschema<3.0.0 -lds-merkle-proof-2019>=0.0.2 \ No newline at end of file +lds-merkle-proof-2019>=0.0.2 From e064c64970ddf5a02723d5e5ac07a77bfbff1684 Mon Sep 17 00:00:00 2001 From: Lucas Parker Date: Fri, 20 Nov 2020 15:03:29 -0800 Subject: [PATCH 27/48] Revving to version 3.0.0b2 --- cert_issuer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cert_issuer/__init__.py b/cert_issuer/__init__.py index c8ce6e55..f153c0d5 100644 --- a/cert_issuer/__init__.py +++ b/cert_issuer/__init__.py @@ -1 +1 @@ -__version__ = '3.0.0b1' +__version__ = '3.0.0b2' From 158916fe26b5247a448224900afed947a1565b5a Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Thu, 7 Jan 2021 15:46:17 +0100 Subject: [PATCH 28/48] test(Validation): add validate_type tests --- tests/test_v3_certificate_validation.py | 103 ++++++++++++++++++ .../test_unit_credential_type.py | 47 ++++++++ .../test_unit_presentation_type.py | 47 ++++++++ 3 files changed, 197 insertions(+) create mode 100644 tests/test_v3_certificate_validation.py create mode 100644 tests/v3_certificate_validation/test_unit_credential_type.py create mode 100644 tests/v3_certificate_validation/test_unit_presentation_type.py diff --git a/tests/test_v3_certificate_validation.py b/tests/test_v3_certificate_validation.py new file mode 100644 index 00000000..540c1c71 --- /dev/null +++ b/tests/test_v3_certificate_validation.py @@ -0,0 +1,103 @@ +import unittest + +import mock +import json +import io +from pycoin.serialize import b2h +from mock import patch, mock_open + +from cert_issuer.certificate_handlers import CertificateWebV3Handler, CertificateV3Handler, CertificateBatchHandler, CertificateHandler, CertificateBatchWebHandler +from cert_issuer.merkle_tree_generator import MerkleTreeGenerator +from cert_issuer import helpers +from cert_core import Chain +from mock import ANY + +class TestV3CertificateValidation(unittest.TestCase): + def _proof_helper(self, chain): + proof = { + 'type': 'MerkleProof2019', + 'created': ANY, + 'proofValue': ANY, + 'proofPurpose': 'assertionMethod', + 'verificationMethod': ANY + } + return proof + + def _helper_mock_call(self, *args): + helper_mock = mock.MagicMock() + helper_mock.__len__.return_value = self.directory_count + + assert args == ( + '/unsigned_certificates_dir', + '/signed_certificates_dir', + '/blockchain_certificates_dir', + '/work_dir') + + return helper_mock + + def _get_certificate_batch_web_handler(self): + secret_manager = mock.Mock() + certificates_to_issue = dict() + certificates_to_issue['1'] = mock.Mock() + certificates_to_issue['2'] = mock.Mock() + certificates_to_issue['3'] = mock.Mock() + + config = mock.Mock() + config.issuing_address = "http://example.com" + + handler = CertificateBatchWebHandler( + secret_manager=secret_manager, + certificate_handler=DummyCertificateHandler(), + merkle_tree=MerkleTreeGenerator(), + config=config) + + return handler, certificates_to_issue + + def _get_certificate_batch_handler(self): + secret_manager = mock.Mock() + certificates_to_issue = dict() + certificates_to_issue['1'] = mock.Mock() + certificates_to_issue['2'] = mock.Mock() + certificates_to_issue['3'] = mock.Mock() + + config = mock.Mock() + config.issuing_address = "http://example.com" + + handler = CertificateBatchHandler( + secret_manager=secret_manager, + certificate_handler=DummyCertificateHandler(), + merkle_tree=MerkleTreeGenerator(), + config=config) + + return handler, certificates_to_issue + + def test_validate_type_valid(self): + certificate = { + 'type': ['VerifiableCredential', 'VerifiablePresentation'] + } + +class DummyCertificateHandler(CertificateHandler): + def __init__(self): + self.config = mock.Mock() + self.config.issuing_address = "http://example.com" + self.counter = 0 + + def _get_certificate_to_issue (self, certificate_metadata): + pass + + def validate_certificate(self, certificate_metadata): + pass + + def sign_certificate(self, signer, certificate_metadata): + pass + + def get_byte_array_to_issue(self, certificate_metadata): + self.counter += 1 + return str(self.counter).encode('utf-8') + + def add_proof(self, certificate_metadata, merkle_proof): + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/v3_certificate_validation/test_unit_credential_type.py b/tests/v3_certificate_validation/test_unit_credential_type.py new file mode 100644 index 00000000..23d4e307 --- /dev/null +++ b/tests/v3_certificate_validation/test_unit_credential_type.py @@ -0,0 +1,47 @@ +import unittest + +from cert_issuer.models import validate_type + +class UnitValidationV3 (unittest.TestCase): + def test_validate_type_valid_credential_type (self): + valid_type = ['VerifiableCredential'] + try: + validate_type(valid_type) + except: + assert False + return + + assert True + + def test_validate_type_invalid_credential_type (self): + valid_type = ['SomethingSomething'] + try: + validate_type(valid_type) + except: + assert True + return + + assert False + + def test_validate_type_invalid_credential_shape (self): + valid_type = 'VerifiableCredential' + try: + validate_type(valid_type) + except: + assert True + return + + assert False + + def test_validate_type_invalid_credential_definition (self): + valid_type = [] + try: + validate_type(valid_type) + except: + assert True + return + + assert False + +if __name__ == '__main__': + unittest.main() diff --git a/tests/v3_certificate_validation/test_unit_presentation_type.py b/tests/v3_certificate_validation/test_unit_presentation_type.py new file mode 100644 index 00000000..b952531e --- /dev/null +++ b/tests/v3_certificate_validation/test_unit_presentation_type.py @@ -0,0 +1,47 @@ +import unittest + +from cert_issuer.models import validate_type + +class UnitValidationV3 (unittest.TestCase): + def test_validate_type_valid_presentation_type (self): + valid_type = ['VerifiablePresentation'] + try: + validate_type(valid_type) + except: + assert False + return + + assert True + + def test_validate_type_invalid_presentation_type (self): + valid_type = ['SomethingSomething'] + try: + validate_type(valid_type) + except: + assert True + return + + assert False + + def test_validate_type_invalid_presentation_shape (self): + valid_type = 'VerifiablePresentation' + try: + validate_type(valid_type) + except: + assert True + return + + assert False + + def test_validate_type_invalid_presentation_definition (self): + valid_type = [] + try: + validate_type(valid_type) + except: + assert True + return + + assert False + +if __name__ == '__main__': + unittest.main() From 74927fefce25a598ca23f6faad38bf6a15de2278 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Tue, 12 Jan 2021 14:38:28 +0100 Subject: [PATCH 29/48] test(Validation): add validate_context tests --- .../test_unit_context.py | 51 +++++++++++++++++++ .../test_unit_credential_type.py | 16 +++--- .../test_unit_presentation_type.py | 16 +++--- 3 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 tests/v3_certificate_validation/test_unit_context.py diff --git a/tests/v3_certificate_validation/test_unit_context.py b/tests/v3_certificate_validation/test_unit_context.py new file mode 100644 index 00000000..d9b97fef --- /dev/null +++ b/tests/v3_certificate_validation/test_unit_context.py @@ -0,0 +1,51 @@ +import unittest + +from cert_issuer.models import validate_context + +class UnitValidationV3 (unittest.TestCase): + def test_validate_context_invalid_shape (self): + candidate_context_url = 'https://www.w3.org/2018/credentials/v1' + candidate_type = ['VerifiableCredential'] + try: + validate_context(candidate_context_url, candidate_type) + except: + assert True + return + + assert False + + def test_validate_context_invalid_order (self): + candidate_context_url = ['link.to.another.context', 'https://www.w3.org/2018/credentials/v1'] + candidate_type = ['VerifiableCredential'] + try: + validate_context(candidate_context_url, candidate_type) + except: + assert True + return + + assert False + + def test_validate_context_invalid_missing_context (self): + candidate_context_url = ['https://www.w3.org/2018/credentials/v1'] + candidate_type = ['VerifiableCredential', 'BlockcertsCredential'] + try: + validate_context(candidate_context_url, candidate_type) + except: + assert True + return + + assert False + + def test_validate_context_valid (self): + candidate_context_url = ['https://www.w3.org/2018/credentials/v1', 'https://www.w3id.org/blockcerts/v3.0-alpha'] + candidate_type = ['VerifiableCredential', 'BlockcertsCredential'] + try: + validate_context(candidate_context_url, candidate_type) + except: + assert False + return + + assert True + +if __name__ == '__main__': + unittest.main() diff --git a/tests/v3_certificate_validation/test_unit_credential_type.py b/tests/v3_certificate_validation/test_unit_credential_type.py index 23d4e307..722c2067 100644 --- a/tests/v3_certificate_validation/test_unit_credential_type.py +++ b/tests/v3_certificate_validation/test_unit_credential_type.py @@ -4,9 +4,9 @@ class UnitValidationV3 (unittest.TestCase): def test_validate_type_valid_credential_type (self): - valid_type = ['VerifiableCredential'] + candidate = ['VerifiableCredential'] try: - validate_type(valid_type) + validate_type(candidate) except: assert False return @@ -14,9 +14,9 @@ def test_validate_type_valid_credential_type (self): assert True def test_validate_type_invalid_credential_type (self): - valid_type = ['SomethingSomething'] + candidate = ['SomethingSomething'] try: - validate_type(valid_type) + validate_type(candidate) except: assert True return @@ -24,9 +24,9 @@ def test_validate_type_invalid_credential_type (self): assert False def test_validate_type_invalid_credential_shape (self): - valid_type = 'VerifiableCredential' + candidate = 'VerifiableCredential' try: - validate_type(valid_type) + validate_type(candidate) except: assert True return @@ -34,9 +34,9 @@ def test_validate_type_invalid_credential_shape (self): assert False def test_validate_type_invalid_credential_definition (self): - valid_type = [] + candidate = [] try: - validate_type(valid_type) + validate_type(candidate) except: assert True return diff --git a/tests/v3_certificate_validation/test_unit_presentation_type.py b/tests/v3_certificate_validation/test_unit_presentation_type.py index b952531e..9e651a69 100644 --- a/tests/v3_certificate_validation/test_unit_presentation_type.py +++ b/tests/v3_certificate_validation/test_unit_presentation_type.py @@ -4,9 +4,9 @@ class UnitValidationV3 (unittest.TestCase): def test_validate_type_valid_presentation_type (self): - valid_type = ['VerifiablePresentation'] + candidate = ['VerifiablePresentation'] try: - validate_type(valid_type) + validate_type(candidate) except: assert False return @@ -14,9 +14,9 @@ def test_validate_type_valid_presentation_type (self): assert True def test_validate_type_invalid_presentation_type (self): - valid_type = ['SomethingSomething'] + candidate = ['SomethingSomething'] try: - validate_type(valid_type) + validate_type(candidate) except: assert True return @@ -24,9 +24,9 @@ def test_validate_type_invalid_presentation_type (self): assert False def test_validate_type_invalid_presentation_shape (self): - valid_type = 'VerifiablePresentation' + candidate = 'VerifiablePresentation' try: - validate_type(valid_type) + validate_type(candidate) except: assert True return @@ -34,9 +34,9 @@ def test_validate_type_invalid_presentation_shape (self): assert False def test_validate_type_invalid_presentation_definition (self): - valid_type = [] + candidate = [] try: - validate_type(valid_type) + validate_type(candidate) except: assert True return From c34b0aab885aa6955cfe838ecc82f5d50ec0f725 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Tue, 12 Jan 2021 15:13:53 +0100 Subject: [PATCH 30/48] test(Validation): add validate_issuer tests --- .../test_unit_issuer.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/v3_certificate_validation/test_unit_issuer.py diff --git a/tests/v3_certificate_validation/test_unit_issuer.py b/tests/v3_certificate_validation/test_unit_issuer.py new file mode 100644 index 00000000..f3d64f57 --- /dev/null +++ b/tests/v3_certificate_validation/test_unit_issuer.py @@ -0,0 +1,37 @@ +import unittest + +from cert_issuer.models import validate_issuer + +class UnitValidationV3 (unittest.TestCase): + def test_validate_issuer_invalid_url (self): + candidate = 'VerifiablePresentation' + try: + validate_issuer(candidate) + except: + assert True + return + + assert True + + def test_validate_issuer_invalid_url_with_space (self): + candidate = 'https:// invalid.url' + try: + validate_issuer(candidate) + except: + assert True + return + + assert False + + def test_validate_issuer_valid_url (self): + candidate = 'https://valid.url' + try: + validate_issuer(candidate) + except: + assert False + return + + assert True + +if __name__ == '__main__': + unittest.main() From 50a93d0f46328baad8e96d46240444b9103ce1b9 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Tue, 12 Jan 2021 15:39:06 +0100 Subject: [PATCH 31/48] test(Validation): add validate_issuance_date tests --- .../test_unit_issuance_date.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/v3_certificate_validation/test_unit_issuance_date.py diff --git a/tests/v3_certificate_validation/test_unit_issuance_date.py b/tests/v3_certificate_validation/test_unit_issuance_date.py new file mode 100644 index 00000000..f73386d2 --- /dev/null +++ b/tests/v3_certificate_validation/test_unit_issuance_date.py @@ -0,0 +1,27 @@ +import unittest + +from cert_issuer.models import validate_issuance_date + +class UnitValidationV3 (unittest.TestCase): + def test_validate_issuance_date_invalid_RFC3339 (self): + candidate = '20200202' + try: + validate_issuance_date(candidate) + except: + assert True + return + + assert False + + def test_validate_issuance_date_valid_RFC3339 (self): + candidate = '2020-02-02T00:00:00Z' + try: + validate_issuance_date(candidate) + except: + assert False + return + + assert True + +if __name__ == '__main__': + unittest.main() From 3142e2867c615986302913a000a5204e497456e7 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Tue, 12 Jan 2021 15:42:29 +0100 Subject: [PATCH 32/48] test(Validation): add validate_expiration_date tests --- .../test_unit_expiration_date.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/v3_certificate_validation/test_unit_expiration_date.py diff --git a/tests/v3_certificate_validation/test_unit_expiration_date.py b/tests/v3_certificate_validation/test_unit_expiration_date.py new file mode 100644 index 00000000..7def9309 --- /dev/null +++ b/tests/v3_certificate_validation/test_unit_expiration_date.py @@ -0,0 +1,27 @@ +import unittest + +from cert_issuer.models import validate_expiration_date + +class UnitValidationV3 (unittest.TestCase): + def test_validate_expiration_date_invalid_RFC3339 (self): + candidate = '20200202' + try: + validate_expiration_date(candidate) + except: + assert True + return + + assert False + + def test_validate_expiration_date_valid_RFC3339 (self): + candidate = '2020-02-02T00:00:00Z' + try: + validate_expiration_date(candidate) + except: + assert False + return + + assert True + +if __name__ == '__main__': + unittest.main() From d5f7b9c4d4f22ab89c59627a9b18270c99c5a258 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 13 Jan 2021 14:48:32 +0100 Subject: [PATCH 33/48] test(Validation): add validate_credential_status tests --- .../test_unit_credential_status.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/v3_certificate_validation/test_unit_credential_status.py diff --git a/tests/v3_certificate_validation/test_unit_credential_status.py b/tests/v3_certificate_validation/test_unit_credential_status.py new file mode 100644 index 00000000..136cf595 --- /dev/null +++ b/tests/v3_certificate_validation/test_unit_credential_status.py @@ -0,0 +1,55 @@ +import unittest +import json + +from cert_issuer.models import validate_credential_status + +class UnitValidationV3 (unittest.TestCase): + def test_validate_credential_status_undefined_id (self): + candidate = { + "type": 'a type' + } + try: + validate_credential_status(candidate) + except: + assert True + return + def test_validate_credential_status_invalid_id (self): + candidate = { + "id": 'not a url', + "type": 'a type' + } + try: + validate_credential_status(candidate) + except: + assert True + return + + assert False + + def test_validate_credential_status_undefined_type (self): + candidate = { + "id": 'https://valid.path' + } + try: + validate_credential_status(candidate) + except: + assert True + return + + assert False + + def test_validate_credential_status_valid (self): + candidate = { + "id": 'https://valid.path', + "type": 'statusList' + } + try: + validate_credential_status(candidate) + except: + assert False + return + + assert True + +if __name__ == '__main__': + unittest.main() From 86402d6854a8409859e6abf44e74a0cf364d4df3 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 13 Jan 2021 16:09:29 +0100 Subject: [PATCH 34/48] test(Validation): add verify_credential tests --- .../test_unit_verify_credential.py | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 tests/v3_certificate_validation/test_unit_verify_credential.py diff --git a/tests/v3_certificate_validation/test_unit_verify_credential.py b/tests/v3_certificate_validation/test_unit_verify_credential.py new file mode 100644 index 00000000..54b27210 --- /dev/null +++ b/tests/v3_certificate_validation/test_unit_verify_credential.py @@ -0,0 +1,170 @@ +import unittest +import copy + +from cert_issuer.models import verify_credential + +credential_example = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.blockcerts.org/schema/3.0-alpha/context.json", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "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", + "credentialSubject": { + "id": "did:key:z6Mkq3L1jEDDZ5R7eT523FMLxC4k6MCpzqD7ff1CrkWpoJwM", + "alumniOf": { + "id": "did:example:c276e12ec21ebfeb1f712ebc6f1" + } + }, + "proof": { + "type": "MerkleProof2019", + "created": "2020-03-11T09:48:20.304161", + "proofValue": "z2LuLBVSfnVzaQtvzuA7EaPQsGEgYWeaMTH1p3uqAG3ESx9HYyFzFFrYsyPkZSbn1Ji5LN76jw6HBr3oiaa8KsQenCPqKk7dJvxEXsDnYvhuDHtsDaQdmCyvpTR9oH46UZcCZ1UY7uZrgHmzf3J8Mzpp5Nnzd4SWiVN4RDWfxSkKmcoXywZ1pTm5bhbKAx1Xeydjwf7T7gcSSkUxQJmfJrWKdyiBjU1vt4oZxwbeTRQ9TfojiDRKJ6RPNsVPpkcDqGvPoaF58SQJG9xr8ACAAH9ZhYXJhRwW2zLpHGdRgyFGdxrcNiBVJ1o1TLcwLsfXTdRZLV2gW5yPLbEui6yBsmHtw9pQkWtfMxGBLzHk5ZRVLMdgUKatiV2QS4oE9N2GyiVnmQomApdS8R2cDSbQdn", + "proofPurpose": "assertionMethod", + "verificationMethod": "ecdsa-koblitz-pubkey:0x7e30a37763e6Ba1fFeDE1750bBeFB4c60b17a1B3" + } +} + +class UnitValidationV3 (unittest.TestCase): + def test_verify_credential_missing_credentialSubject (self): + candidate = copy.deepcopy(credential_example) + del candidate['credentialSubject'] + try: + verify_credential(candidate) + except: + assert True + return + + assert False + + def test_verify_credential_missing_issuer (self): + candidate = copy.deepcopy(credential_example) + del candidate['issuer'] + try: + verify_credential(candidate) + except: + assert True + return + + assert False + + def test_verify_credential_invalid_issuer (self): + candidate = copy.deepcopy(credential_example) + candidate['issuer'] = 'not a url' + try: + verify_credential(candidate) + except: + assert True + return + + assert False + + def test_verify_credential_missing_issuance_date (self): + candidate = copy.deepcopy(credential_example) + del candidate['issuanceDate'] + try: + verify_credential(candidate) + except: + assert True + return + + assert False + + def test_verify_credential_invalid_issuance_date (self): + candidate = copy.deepcopy(credential_example) + candidate['issuanceDate'] = '20200202' + try: + verify_credential(candidate) + except: + assert True + return + + assert False + + def test_verify_credential_missing_optional_expiration_date (self): + candidate = copy.deepcopy(credential_example) + try: + verify_credential(candidate) + except: + assert False + return + + assert True + + def test_verify_credential_invalid_expiration_date (self): + candidate = copy.deepcopy(credential_example) + candidate['expirationDate'] = '20200202' + try: + verify_credential(candidate) + except: + assert True + return + + assert False + + def test_verify_credential_valid_expiration_date (self): + candidate = copy.deepcopy(credential_example) + candidate['expirationDate'] = '2020-02-02T00:00:00Z' + try: + verify_credential(candidate) + except: + assert False + return + + assert True + + def test_verify_credential_missing_optional_credential_status (self): + candidate = copy.deepcopy(credential_example) + try: + verify_credential(candidate) + except: + assert False + return + + assert True + + def test_verify_credential_invalid_credential_status (self): + candidate = copy.deepcopy(credential_example) + candidate['credentialStatus'] = { + 'invalid': True + } + try: + verify_credential(candidate) + except: + assert True + return + + assert False + + def test_verify_credential_valid_credential_status (self): + candidate = copy.deepcopy(credential_example) + candidate['credentialStatus'] = { + 'id': 'https://valid.path', + 'type': 'statusList' + } + try: + verify_credential(candidate) + except: + assert False + return + + assert True + + def test_verify_credential_valid (self): + candidate = copy.deepcopy(credential_example) + try: + verify_credential(candidate) + except: + assert False + return + + assert True + +if __name__ == '__main__': + unittest.main() From 12977d7449e59bf8ce8d9b289b82f1008c01b2b1 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 13 Jan 2021 16:50:08 +0100 Subject: [PATCH 35/48] chore(Tests): remove deprecation warnings --- tests/test_connectors.py | 12 ++++++------ tests/test_tx_utils.py | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_connectors.py b/tests/test_connectors.py index 085345e7..7db3d580 100644 --- a/tests/test_connectors.py +++ b/tests/test_connectors.py @@ -60,12 +60,12 @@ def test_bitcoind_connector_spendables(self): SelectParams('testnet') bc = BitcoindConnector('testnet') spendables = bc.spendables_for_address('mz7poFND7hVGRtPWjiZizcCnjf6wEDWjjT') - self.assertEquals(len(spendables), 3) - self.assertEquals(b2h(spendables[0].tx_hash), + self.assertEqual(len(spendables), 3) + self.assertEqual(b2h(spendables[0].tx_hash), '08f6528ac70c828e1633babc8f0d49ecb11649fd7451f76923821a0dbc81eb34') - self.assertEquals(spendables[0].coin_value, 49000000) - self.assertEquals(spendables[1].coin_value, 2750) - self.assertEquals(spendables[2].coin_value, 2750) + self.assertEqual(spendables[0].coin_value, 49000000) + self.assertEqual(spendables[1].coin_value, 2750) + self.assertEqual(spendables[2].coin_value, 2750) # TODO: this test isn't calling the bitcoin RPC proxy because of the changed configuration. This will most likely # need to be different in the open source. Fix this test and connectors. @@ -73,7 +73,7 @@ def test_bitcoind_connector_spendables(self): # bitcoin.SelectParams('testnet') # connector = ServiceProviderConnector('XTN', 'na') # balance = connector.get_balance('mz7poFND7hVGRtPWjiZizcCnjf6wEDWjjT') - # self.assertEquals(balance, 49005500) + # self.assertEqual(balance, 49005500) if __name__ == '__main__': diff --git a/tests/test_tx_utils.py b/tests/test_tx_utils.py index 3dcfe8d5..6b180dbe 100644 --- a/tests/test_tx_utils.py +++ b/tests/test_tx_utils.py @@ -30,15 +30,15 @@ def test_calculate_raw_tx_size(self): def test_calculate_raw_tx_size_with_op_return(self): estimated_byte_count = tx_utils.calculate_raw_tx_size_with_op_return(num_inputs=1, num_outputs=600) - self.assertEquals(estimated_byte_count, 20602) + self.assertEqual(estimated_byte_count, 20602) def test_calculate_raw_tx_size_with_op_return_2(self): estimated_byte_count = tx_utils.calculate_raw_tx_size_with_op_return(num_inputs=1, num_outputs=2000) - self.assertEquals(estimated_byte_count, 68202) + self.assertEqual(estimated_byte_count, 68202) def test_calculate_raw_tx_size_with_op_return_3(self): estimated_byte_count = tx_utils.calculate_raw_tx_size_with_op_return(num_inputs=1, num_outputs=4000) - self.assertEquals(estimated_byte_count, 136202) + self.assertEqual(estimated_byte_count, 136202) def test_create_trx(self): SelectParams('testnet') @@ -48,7 +48,7 @@ def test_create_trx(self): tx_outs = [tx_utils.create_transaction_output('mgAqW5ZCnEp7fjvpj8RUL3WxsBy8rcDcCi', 0.0000275)] tx = tx_utils.create_trx('TEST'.encode('utf-8'), 3, 'mgAqW5ZCnEp7fjvpj8RUL3WxsBy8rcDcCi', tx_outs, [tx_input]) hextx = b2h(tx.serialize()) - self.assertEquals(hextx, + self.assertEqual(hextx, '01000000018443b07464c762d7fb404ea918a5ac9b3618d5cd6a0c5ea6e4dd5d7bbe28b1540000000000ffffffff0300000000000000001976a914072a22e5913cd939904c46bbd0bc56755543384b88acc5000000000000001976a914072a22e5913cd939904c46bbd0bc56755543384b88ac0000000000000000066a045445535400000000') def test_compare_cost(self): @@ -68,7 +68,7 @@ def test_compare_cost(self): tx_byte_count = len(s.getvalue()) estimated_byte_count = tx_utils.calculate_raw_tx_size_with_op_return(num_inputs=1, num_outputs=6) - self.assertEquals(estimated_byte_count, tx_byte_count + 1) + self.assertEqual(estimated_byte_count, tx_byte_count + 1) def test_calculate_tx_fee_1(self): cost_constants = BitcoinTransactionCostConstants(0.0001, 0.0000275, 41) From 872004840cbf8bed355437c58be6ecd55b6a9b92 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 13 Jan 2021 17:13:23 +0100 Subject: [PATCH 36/48] test(Validation): add verify_presentation tests --- .../test_unit_verify_credential.py | 7 -- .../test_unit_verify_presentation.py | 89 +++++++++++++++++++ 2 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 tests/v3_certificate_validation/test_unit_verify_presentation.py diff --git a/tests/v3_certificate_validation/test_unit_verify_credential.py b/tests/v3_certificate_validation/test_unit_verify_credential.py index 54b27210..137efdbf 100644 --- a/tests/v3_certificate_validation/test_unit_verify_credential.py +++ b/tests/v3_certificate_validation/test_unit_verify_credential.py @@ -21,13 +21,6 @@ "alumniOf": { "id": "did:example:c276e12ec21ebfeb1f712ebc6f1" } - }, - "proof": { - "type": "MerkleProof2019", - "created": "2020-03-11T09:48:20.304161", - "proofValue": "z2LuLBVSfnVzaQtvzuA7EaPQsGEgYWeaMTH1p3uqAG3ESx9HYyFzFFrYsyPkZSbn1Ji5LN76jw6HBr3oiaa8KsQenCPqKk7dJvxEXsDnYvhuDHtsDaQdmCyvpTR9oH46UZcCZ1UY7uZrgHmzf3J8Mzpp5Nnzd4SWiVN4RDWfxSkKmcoXywZ1pTm5bhbKAx1Xeydjwf7T7gcSSkUxQJmfJrWKdyiBjU1vt4oZxwbeTRQ9TfojiDRKJ6RPNsVPpkcDqGvPoaF58SQJG9xr8ACAAH9ZhYXJhRwW2zLpHGdRgyFGdxrcNiBVJ1o1TLcwLsfXTdRZLV2gW5yPLbEui6yBsmHtw9pQkWtfMxGBLzHk5ZRVLMdgUKatiV2QS4oE9N2GyiVnmQomApdS8R2cDSbQdn", - "proofPurpose": "assertionMethod", - "verificationMethod": "ecdsa-koblitz-pubkey:0x7e30a37763e6Ba1fFeDE1750bBeFB4c60b17a1B3" } } diff --git a/tests/v3_certificate_validation/test_unit_verify_presentation.py b/tests/v3_certificate_validation/test_unit_verify_presentation.py new file mode 100644 index 00000000..0adf03e0 --- /dev/null +++ b/tests/v3_certificate_validation/test_unit_verify_presentation.py @@ -0,0 +1,89 @@ +import unittest +import copy + +from cert_issuer.models import verify_presentation + +presentation_example = { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.blockcerts.org/schema/3.0-alpha/context.json", + "https://www.w3.org/2018/credentials/examples/v1", + { + "metadataJson": { + "@id": "https://schemas.learningmachine.com/2017/blockcerts/metadata", + "@type": "https://schemas.learningmachine.com/2017/types/text/json" + }, + "displayHtml": { + "@id": "https://schemas.learningmachine.com/2017/blockcerts/displayHtml", + "@type": "https://schemas.learningmachine.com/2017/types/text/html" + }, + "nonce": { + "@id": "https://schemas.learningmachine.com/2017/blockcerts/nonce", + "@type": "https://schema.org/Text" + }, + "universalIdentifier": { + "@id": "https://schemas.learningmachine.com/2017/blockcerts/identifier", + "@type": "https://schema.org/Text" + } + } + ], + "id": "urn:uuid:bbba8553-8ec1-445f-82c9-a57251dd731c", + "metadataJson": "{\"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\"]}", + "displayHtml": "hello world", + "nonce": "814ce340-12f3-414b-af91-a0f9489e5dbc", + "universalIdentifier": "ab569127-34bb-5784-bced-00b7e0e82ac9", + "type": [ + "VerifiableCredential", + "BlockcertsCredential" + ], + "issuer": "https://raw.githubusercontent.com/AnthonyRonning/https-github.com-labnol-files/master/issuer-eth.json", + "issuanceDate": "2010-01-01T19:33:24Z", + "credentialSubject": { + "id": "did:key:z6Mkq3L1jEDDZ5R7eT523FMLxC4k6MCpzqD7ff1CrkWpoJwM", + "alumniOf": { + "id": "did:example:c276e12ec21ebfeb1f712ebc6f1" + } + }, + "proof": { + "type": "MerkleProof2019", + "created": "2020-03-23T15:38:11.804838", + "proofValue": "z2LuLBVSfnVzaQtvzuA7EaPQsGEgYWeaMTH1p3uqAG3ESx9HYyFzFFrYsyPkZSbn1Ji5LN76jw6HBr3oiaa8KsQenCPqKk7dJvxEXsDnYvhuDHu3ktTZuz4KL2UWU3hieKFwMG2akp4rPvYmwQDbtXNmhZgpdGpp9hiDZiz37bca2LZZG2VJ9Xen31trVG5A2SApCkFoUxYeNvXr8reqJPca1voRwFXAgo25XWV2BQ1ycQ2wM3jPz3BAx4tZuPno7Ebd5XLfroXHCaKiNadiqxLedp2SHZjDicG8kxMwPo2gR1mYeWjtQSPVMrtf6p325wCNVrQpxTAszLp4CPXSZFFYsb2dn9iRAcMTUSKYhYtsNjst2fDdPye4arHmvLL5s6pL6U8vtEEBiYJDrFj8xo", + "proofPurpose": "assertionMethod", + "verificationMethod": "ecdsa-koblitz-pubkey:0x7e30a37763e6Ba1fFeDE1750bBeFB4c60b17a1B3" + } + } + ] +} + +class UnitValidationV3 (unittest.TestCase): + def test_verify_presentation_invalid_credential (self): + candidate = copy.deepcopy(presentation_example) + del candidate['verifiableCredential'][0]['credentialSubject'] + try: + verify_presentation(candidate) + except: + assert True + return + + assert False + + def test_verify_presentation_valid_credential (self): + candidate = copy.deepcopy(presentation_example) + try: + verify_presentation(candidate) + except: + assert False + return + + assert True + +if __name__ == '__main__': + unittest.main() From fdd81047a52ae3a93861e4a7b23e9a714b31bb3a Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Fri, 22 Jan 2021 17:56:31 +0100 Subject: [PATCH 37/48] test(V3): scaffhold integration test VC compliance --- cert_issuer/certificate_handlers.py | 4 + cert_issuer/models.py | 1 + ...handler.py => nope_certificate_handler.py} | 0 tests/test_v3_certificate_validation.py | 149 ++++++------------ 4 files changed, 57 insertions(+), 97 deletions(-) rename tests/{test_certificate_handler.py => nope_certificate_handler.py} (100%) diff --git a/cert_issuer/certificate_handlers.py b/cert_issuer/certificate_handlers.py index 11f14344..8369a71b 100644 --- a/cert_issuer/certificate_handlers.py +++ b/cert_issuer/certificate_handlers.py @@ -28,6 +28,7 @@ def add_proof(self, certificate_metadata, merkle_proof): out_file.write(json.dumps(certificate_json)) def _get_certificate_to_issue(self, certificate_metadata): + print('not mock') with open(certificate_metadata.unsigned_cert_file_name, 'r') as unsigned_cert_file: certificate_json = json.load(unsigned_cert_file) return certificate_json @@ -99,7 +100,10 @@ def prepare_batch(self): # validate batch for _, metadata in self.certificates_to_issue.items(): + print(self.certificate_handler) certificate_json = self.certificate_handler._get_certificate_to_issue(metadata) + print(certificate_json) + raise ValueError(certificate_json) self.certificate_handler.validate_certificate(certificate_json) # sign batch diff --git a/cert_issuer/models.py b/cert_issuer/models.py index 1c9dc15b..98ab8e44 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -155,6 +155,7 @@ def set_certificates_in_batch(self, certificates_to_issue): class CertificateHandler(object): @abstractmethod def validate_certificate(self, certificate_metadata): + raise ValueError('yo yo yo') validate_type(certificate_metadata['type']) validate_context(certificate_metadata['@context'], certificate_metadata['type']) diff --git a/tests/test_certificate_handler.py b/tests/nope_certificate_handler.py similarity index 100% rename from tests/test_certificate_handler.py rename to tests/nope_certificate_handler.py diff --git a/tests/test_v3_certificate_validation.py b/tests/test_v3_certificate_validation.py index 540c1c71..e4ef7dcf 100644 --- a/tests/test_v3_certificate_validation.py +++ b/tests/test_v3_certificate_validation.py @@ -1,103 +1,58 @@ import unittest - import mock -import json -import io -from pycoin.serialize import b2h -from mock import patch, mock_open - -from cert_issuer.certificate_handlers import CertificateWebV3Handler, CertificateV3Handler, CertificateBatchHandler, CertificateHandler, CertificateBatchWebHandler -from cert_issuer.merkle_tree_generator import MerkleTreeGenerator -from cert_issuer import helpers -from cert_core import Chain -from mock import ANY - -class TestV3CertificateValidation(unittest.TestCase): - def _proof_helper(self, chain): - proof = { - 'type': 'MerkleProof2019', - 'created': ANY, - 'proofValue': ANY, - 'proofPurpose': 'assertionMethod', - 'verificationMethod': ANY - } - return proof - - def _helper_mock_call(self, *args): - helper_mock = mock.MagicMock() - helper_mock.__len__.return_value = self.directory_count - - assert args == ( - '/unsigned_certificates_dir', - '/signed_certificates_dir', - '/blockchain_certificates_dir', - '/work_dir') - - return helper_mock - - def _get_certificate_batch_web_handler(self): - secret_manager = mock.Mock() - certificates_to_issue = dict() - certificates_to_issue['1'] = mock.Mock() - certificates_to_issue['2'] = mock.Mock() - certificates_to_issue['3'] = mock.Mock() - - config = mock.Mock() - config.issuing_address = "http://example.com" - - handler = CertificateBatchWebHandler( - secret_manager=secret_manager, - certificate_handler=DummyCertificateHandler(), - merkle_tree=MerkleTreeGenerator(), - config=config) - - return handler, certificates_to_issue - - def _get_certificate_batch_handler(self): - secret_manager = mock.Mock() - certificates_to_issue = dict() - certificates_to_issue['1'] = mock.Mock() - certificates_to_issue['2'] = mock.Mock() - certificates_to_issue['3'] = mock.Mock() - - config = mock.Mock() - config.issuing_address = "http://example.com" - +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.blockcerts.org/schema/3.0-alpha/context.json", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "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", + "credentialSubject": { + "id": "did:key:z6Mkq3L1jEDDZ5R7eT523FMLxC4k6MCpzqD7ff1CrkWpoJwM", + "alumniOf": { + "id": "did:example:c276e12ec21ebfeb1f712ebc6f1" + } + } +} + +class TestCertificateV3Validation(unittest.TestCase): + def missing_credential_subject (self): + candidate = copy.deepcopy(credential_example) + del candidate['credentialSubject'] + handler.certificates_to_issue = [mock.Mock()] handler = CertificateBatchHandler( - secret_manager=secret_manager, - certificate_handler=DummyCertificateHandler(), - merkle_tree=MerkleTreeGenerator(), - config=config) - - return handler, certificates_to_issue - - def test_validate_type_valid(self): - certificate = { - 'type': ['VerifiableCredential', 'VerifiablePresentation'] - } - -class DummyCertificateHandler(CertificateHandler): - def __init__(self): - self.config = mock.Mock() - self.config.issuing_address = "http://example.com" - self.counter = 0 - - def _get_certificate_to_issue (self, certificate_metadata): - pass - - def validate_certificate(self, certificate_metadata): - pass - - def sign_certificate(self, signer, certificate_metadata): - pass - - def get_byte_array_to_issue(self, certificate_metadata): - self.counter += 1 - return str(self.counter).encode('utf-8') - - def add_proof(self, certificate_metadata, merkle_proof): - pass - + secret_manager=mock.Mock(), + certificate_handler=MockCertificateV3Handler(candidate), + merkle_tree=mock.Mock(), + config=mock.Mock() + ) + + try: + handler.prepare_batch() + except: + assert False + return + + assert True + +class MockCertificateV3Handler(CertificateV3Handler): + def __init__(self, test_certificate): + raise ValueError('mock init'); + self.test_certificate = test_certificate + def _get_certificate_to_issue(data): + print('mock call') + return self.test_certificate if __name__ == '__main__': unittest.main() From 87ea60c36e0ce89d01c8c0c6805bd2e9b1a43a09 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Mon, 25 Jan 2021 14:52:18 +0100 Subject: [PATCH 38/48] test(V3): integration test VC compliance passing --- cert_issuer/certificate_handlers.py | 3 --- cert_issuer/models.py | 1 - ...e_handler.py => test_certificate_handler.py} | 0 ...st_integration_batch_issuance_validation.py} | 17 ++++++++--------- 4 files changed, 8 insertions(+), 13 deletions(-) rename tests/{nope_certificate_handler.py => test_certificate_handler.py} (100%) rename tests/{test_v3_certificate_validation.py => v3_certificate_validation/test_integration_batch_issuance_validation.py} (79%) diff --git a/cert_issuer/certificate_handlers.py b/cert_issuer/certificate_handlers.py index 8369a71b..51394079 100644 --- a/cert_issuer/certificate_handlers.py +++ b/cert_issuer/certificate_handlers.py @@ -100,10 +100,7 @@ def prepare_batch(self): # validate batch for _, metadata in self.certificates_to_issue.items(): - print(self.certificate_handler) certificate_json = self.certificate_handler._get_certificate_to_issue(metadata) - print(certificate_json) - raise ValueError(certificate_json) self.certificate_handler.validate_certificate(certificate_json) # sign batch diff --git a/cert_issuer/models.py b/cert_issuer/models.py index 98ab8e44..1c9dc15b 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -155,7 +155,6 @@ def set_certificates_in_batch(self, certificates_to_issue): class CertificateHandler(object): @abstractmethod def validate_certificate(self, certificate_metadata): - raise ValueError('yo yo yo') validate_type(certificate_metadata['type']) validate_context(certificate_metadata['@context'], certificate_metadata['type']) diff --git a/tests/nope_certificate_handler.py b/tests/test_certificate_handler.py similarity index 100% rename from tests/nope_certificate_handler.py rename to tests/test_certificate_handler.py diff --git a/tests/test_v3_certificate_validation.py b/tests/v3_certificate_validation/test_integration_batch_issuance_validation.py similarity index 79% rename from tests/test_v3_certificate_validation.py rename to tests/v3_certificate_validation/test_integration_batch_issuance_validation.py index e4ef7dcf..31060b8b 100644 --- a/tests/test_v3_certificate_validation.py +++ b/tests/v3_certificate_validation/test_integration_batch_issuance_validation.py @@ -26,32 +26,31 @@ } } -class TestCertificateV3Validation(unittest.TestCase): - def missing_credential_subject (self): +class TestIssuanceBatchValidation (unittest.TestCase): + def test_missing_credential_subject (self): candidate = copy.deepcopy(credential_example) del candidate['credentialSubject'] - handler.certificates_to_issue = [mock.Mock()] handler = CertificateBatchHandler( secret_manager=mock.Mock(), certificate_handler=MockCertificateV3Handler(candidate), merkle_tree=mock.Mock(), config=mock.Mock() ) + handler.certificates_to_issue = {'metadata': mock.Mock()} try: handler.prepare_batch() - except: - assert False + except Exception as e: + self.assertEqual(str(e), '`credentialSubject property must be defined`') return - assert True + assert False class MockCertificateV3Handler(CertificateV3Handler): def __init__(self, test_certificate): - raise ValueError('mock init'); self.test_certificate = test_certificate - def _get_certificate_to_issue(data): - print('mock call') + print(self.test_certificate) + def _get_certificate_to_issue(self, data): return self.test_certificate if __name__ == '__main__': From ad52583be48319597c3a0b95cce29a6b7abf788e Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Mon, 25 Jan 2021 18:06:41 +0100 Subject: [PATCH 39/48] test(V3): test whole VC verification execution --- cert_issuer/models.py | 10 +- ...t_integration_batch_issuance_validation.py | 120 +++++++++++++++++- 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/cert_issuer/models.py b/cert_issuer/models.py index 1c9dc15b..d082b234 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -31,7 +31,7 @@ def validate_context (context, type): vc_context_url = 'https://www.w3.org/2018/credentials/v1' if not isinstance(context, list): - raise ValueError('@context property must be an array') + raise ValueError('`@context` property must be an array') if context[0] != vc_context_url: raise ValueError('First @context declared must be {}, was given {}'.format(vc_context_url, context[0])) if len(type) > 1 and len(context) == 1: @@ -50,7 +50,7 @@ def validate_issuer (certificate_issuer): pass def validate_date_RFC3339_string_format (date, property_name): - error_message = '{} property must be a valid RFC3339 string'.format(property_name) + error_message = '`{}` property must be a valid RFC3339 string'.format(property_name) if not isinstance(date, str): raise ValueError(error_message) @@ -87,13 +87,13 @@ def verify_credential(certificate_metadata): # if undefined will throw KeyError validate_credential_subject(certificate_metadata['credentialSubject']) except: - raise ValueError('`credentialSubject property must be defined`') + raise ValueError('`credentialSubject` property must be defined') try: # if undefined will throw KeyError validate_issuer(certificate_metadata['issuer']) except KeyError: - raise ValueError('`issuer property must be defined`') + raise ValueError('`issuer` property must be defined') except ValueError as err: raise ValueError(err) @@ -101,7 +101,7 @@ def verify_credential(certificate_metadata): # if undefined will throw KeyError validate_issuance_date(certificate_metadata['issuanceDate']) except KeyError: - raise ValueError('`issuance_date property must be defined`') + raise ValueError('`issuanceDate` property must be defined') except ValueError as err: raise ValueError(err) diff --git a/tests/v3_certificate_validation/test_integration_batch_issuance_validation.py b/tests/v3_certificate_validation/test_integration_batch_issuance_validation.py index 31060b8b..48acc234 100644 --- a/tests/v3_certificate_validation/test_integration_batch_issuance_validation.py +++ b/tests/v3_certificate_validation/test_integration_batch_issuance_validation.py @@ -27,7 +27,45 @@ } class TestIssuanceBatchValidation (unittest.TestCase): - def test_missing_credential_subject (self): + def test_verify_type (self): + candidate = copy.deepcopy(credential_example) + candidate['type'] = 'Invalid Shape' + handler = CertificateBatchHandler( + secret_manager=mock.Mock(), + certificate_handler=MockCertificateV3Handler(candidate), + 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), '`type` property must be an array') + return + + assert False + + def test_verify_context (self): + candidate = copy.deepcopy(credential_example) + candidate['@context'] = 'Invalid Shape' + handler = CertificateBatchHandler( + secret_manager=mock.Mock(), + certificate_handler=MockCertificateV3Handler(candidate), + 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), '`@context` property must be an array') + return + + assert False + + def test_verify_credential_subject (self): candidate = copy.deepcopy(credential_example) del candidate['credentialSubject'] handler = CertificateBatchHandler( @@ -41,7 +79,85 @@ def test_missing_credential_subject (self): try: handler.prepare_batch() except Exception as e: - self.assertEqual(str(e), '`credentialSubject property must be defined`') + self.assertEqual(str(e), '`credentialSubject` property must be defined') + return + + assert False + + def test_verify_issuer (self): + candidate = copy.deepcopy(credential_example) + del candidate['issuer'] + handler = CertificateBatchHandler( + secret_manager=mock.Mock(), + certificate_handler=MockCertificateV3Handler(candidate), + 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), '`issuer` property must be defined') + return + + assert False + + def test_verify_issuance_date (self): + candidate = copy.deepcopy(credential_example) + del candidate['issuanceDate'] + handler = CertificateBatchHandler( + secret_manager=mock.Mock(), + certificate_handler=MockCertificateV3Handler(candidate), + 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), '`issuanceDate` property must be defined') + return + + assert False + + def test_verify_expiration_date (self): + candidate = copy.deepcopy(credential_example) + candidate['expirationDate'] = '20200909' + handler = CertificateBatchHandler( + secret_manager=mock.Mock(), + certificate_handler=MockCertificateV3Handler(candidate), + 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), '`expirationDate` property must be a valid RFC3339 string') + return + + assert False + + def test_verify_credential_status (self): + candidate = copy.deepcopy(credential_example) + candidate['credentialStatus'] = { + "id": 'https://valid.path' + } + handler = CertificateBatchHandler( + secret_manager=mock.Mock(), + certificate_handler=MockCertificateV3Handler(candidate), + 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), 'credentialStatus.type must be defined') return assert False From 2a6026f48f8c0a62bc99d4f6a5d4cd96474eb6b2 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Mon, 25 Jan 2021 19:56:22 +0100 Subject: [PATCH 40/48] test(V3): test whole VP verification execution --- ... => test_integration_verify_credential.py} | 0 .../test_integration_verify_presentation.py | 96 +++++++++++++++++++ 2 files changed, 96 insertions(+) rename tests/v3_certificate_validation/{test_integration_batch_issuance_validation.py => test_integration_verify_credential.py} (100%) create mode 100644 tests/v3_certificate_validation/test_integration_verify_presentation.py diff --git a/tests/v3_certificate_validation/test_integration_batch_issuance_validation.py b/tests/v3_certificate_validation/test_integration_verify_credential.py similarity index 100% rename from tests/v3_certificate_validation/test_integration_batch_issuance_validation.py rename to tests/v3_certificate_validation/test_integration_verify_credential.py diff --git a/tests/v3_certificate_validation/test_integration_verify_presentation.py b/tests/v3_certificate_validation/test_integration_verify_presentation.py new file mode 100644 index 00000000..4ce6deff --- /dev/null +++ b/tests/v3_certificate_validation/test_integration_verify_presentation.py @@ -0,0 +1,96 @@ +import unittest +import mock +import copy + +from cert_issuer.certificate_handlers import CertificateBatchHandler, CertificateV3Handler +from cert_issuer.models import CertificateHandler + +presentation_example = { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.blockcerts.org/schema/3.0-alpha/context.json", + "https://www.w3.org/2018/credentials/examples/v1", + { + "metadataJson": { + "@id": "https://schemas.learningmachine.com/2017/blockcerts/metadata", + "@type": "https://schemas.learningmachine.com/2017/types/text/json" + }, + "displayHtml": { + "@id": "https://schemas.learningmachine.com/2017/blockcerts/displayHtml", + "@type": "https://schemas.learningmachine.com/2017/types/text/html" + }, + "nonce": { + "@id": "https://schemas.learningmachine.com/2017/blockcerts/nonce", + "@type": "https://schema.org/Text" + }, + "universalIdentifier": { + "@id": "https://schemas.learningmachine.com/2017/blockcerts/identifier", + "@type": "https://schema.org/Text" + } + } + ], + "id": "urn:uuid:bbba8553-8ec1-445f-82c9-a57251dd731c", + "metadataJson": "{\"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\"]}", + "displayHtml": "hello world", + "nonce": "814ce340-12f3-414b-af91-a0f9489e5dbc", + "universalIdentifier": "ab569127-34bb-5784-bced-00b7e0e82ac9", + "type": [ + "VerifiableCredential", + "BlockcertsCredential" + ], + "issuer": "https://raw.githubusercontent.com/AnthonyRonning/https-github.com-labnol-files/master/issuer-eth.json", + "issuanceDate": "2010-01-01T19:33:24Z", + "credentialSubject": { + "id": "did:key:z6Mkq3L1jEDDZ5R7eT523FMLxC4k6MCpzqD7ff1CrkWpoJwM", + "alumniOf": { + "id": "did:example:c276e12ec21ebfeb1f712ebc6f1" + } + }, + "proof": { + "type": "MerkleProof2019", + "created": "2020-03-23T15:38:11.804838", + "proofValue": "z2LuLBVSfnVzaQtvzuA7EaPQsGEgYWeaMTH1p3uqAG3ESx9HYyFzFFrYsyPkZSbn1Ji5LN76jw6HBr3oiaa8KsQenCPqKk7dJvxEXsDnYvhuDHu3ktTZuz4KL2UWU3hieKFwMG2akp4rPvYmwQDbtXNmhZgpdGpp9hiDZiz37bca2LZZG2VJ9Xen31trVG5A2SApCkFoUxYeNvXr8reqJPca1voRwFXAgo25XWV2BQ1ycQ2wM3jPz3BAx4tZuPno7Ebd5XLfroXHCaKiNadiqxLedp2SHZjDicG8kxMwPo2gR1mYeWjtQSPVMrtf6p325wCNVrQpxTAszLp4CPXSZFFYsb2dn9iRAcMTUSKYhYtsNjst2fDdPye4arHmvLL5s6pL6U8vtEEBiYJDrFj8xo", + "proofPurpose": "assertionMethod", + "verificationMethod": "ecdsa-koblitz-pubkey:0x7e30a37763e6Ba1fFeDE1750bBeFB4c60b17a1B3" + } + } + ] +} + +class TestIssuanceBatchValidation (unittest.TestCase): + def test_verify_credential_status (self): + candidate = copy.deepcopy(presentation_example) + del candidate['verifiableCredential'][0]['credentialSubject'] + handler = CertificateBatchHandler( + secret_manager=mock.Mock(), + certificate_handler=MockCertificateV3Handler(candidate), + 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), 'A Verifiable Presentation must contain valid verifiableCredential(s)') + 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() From e83deb54e7e4ab31395c628afb1fb4a4fbb243e2 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Mon, 25 Jan 2021 19:58:34 +0100 Subject: [PATCH 41/48] style(V3): remove trailing print --- cert_issuer/certificate_handlers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cert_issuer/certificate_handlers.py b/cert_issuer/certificate_handlers.py index 51394079..11f14344 100644 --- a/cert_issuer/certificate_handlers.py +++ b/cert_issuer/certificate_handlers.py @@ -28,7 +28,6 @@ def add_proof(self, certificate_metadata, merkle_proof): out_file.write(json.dumps(certificate_json)) def _get_certificate_to_issue(self, certificate_metadata): - print('not mock') with open(certificate_metadata.unsigned_cert_file_name, 'r') as unsigned_cert_file: certificate_json = json.load(unsigned_cert_file) return certificate_json From e46702c40276c1ed51f97c90e692e683f932879d Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Mon, 4 Oct 2021 17:42:29 +0200 Subject: [PATCH 42/48] feat(VerifiableCredential): allow for issuer object with id as per the spec --- cert_issuer/models.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/cert_issuer/models.py b/cert_issuer/models.py index d082b234..e3d5e526 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -7,13 +7,17 @@ def validate_RFC3339_date (date): return re.match('^[1-9]\d{3}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}[Zz]$', date) -def validate_url (url): - parsed_url = urlparse(url) - is_valid_url = (not (parsed_url.path.__contains__(' ') - or parsed_url.netloc.__contains__(' ')) - and url.__contains__(':')) +def is_valid_url (url): + try: + parsed_url = urlparse(url) + except: + return False + return (not (parsed_url.path.__contains__(' ') + or parsed_url.netloc.__contains__(' ')) + and url.__contains__(':')) - if not is_valid_url: +def validate_url (url): + if not is_valid_url (parsed_url): raise ValueError('Invalid URL: {}'.format(url)) pass @@ -43,10 +47,8 @@ def validate_credential_subject (credential_subject): pass def validate_issuer (certificate_issuer): - try: - validate_url(certificate_issuer) - except: - raise ValueError('`issuer` property must be a URL string') + if not is_valid_url(certificate_issuer) and not is_valid_url(certificate_issuer['id']): + raise ValueError('`issuer` property must be a URL string or an object with an `id` property containing a URL string') pass def validate_date_RFC3339_string_format (date, property_name): From 8b7a1b96ecd9854d33543bab7cae4035783caa90 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 6 Oct 2021 14:49:41 +0200 Subject: [PATCH 43/48] feat(Schema): bump cert-schema --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2d5f7d93..c5703f79 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cert-core>=3.0.0b1 -cert-schema>=3.0.0b1 +cert-schema>=3.0.0b4 merkletools==1.0.3 configargparse==0.12.0 glob2==0.6 From 2998dd6a1693ef49c930948a20f00dbf79fdb3ef Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 1 Dec 2021 15:28:30 +0100 Subject: [PATCH 44/48] fix(Etherscan): [#205] prevent captcha request when calling Etherscan ropsten api --- .../blockchain_handlers/ethereum/connectors.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/cert_issuer/blockchain_handlers/ethereum/connectors.py b/cert_issuer/blockchain_handlers/ethereum/connectors.py index 9575e055..d5ffc717 100644 --- a/cert_issuer/blockchain_handlers/ethereum/connectors.py +++ b/cert_issuer/blockchain_handlers/ethereum/connectors.py @@ -55,7 +55,7 @@ def __init__( if hasattr(app_config, 'ropsten_rpc_url'): self.ropsten_rpc_url = app_config.ropsten_rpc_url rop_provider_list.append(EthereumRPCProvider(self.ropsten_rpc_url)) - rop_provider_list.append(EtherscanBroadcaster('https://ropsten.etherscan.io/api', etherscan_api_token)) + rop_provider_list.append(EtherscanBroadcaster('https://api-ropsten.etherscan.io/api', etherscan_api_token)) # rop_provider_list.append(MyEtherWalletBroadcaster('https://api.myetherwallet.com/rop', None)) self.connectors[Chain.ethereum_ropsten] = rop_provider_list @@ -137,8 +137,8 @@ def get_balance(self, address): """ Returns the balance in Wei. """ - logging.info('Getting balance with EthereumRPCProvider') response = self.w3.eth.getBalance(account=address, block_identifier="latest") + logging.info('Getting balance with EthereumRPCProvider: %s', response) return response def get_address_nonce(self, address): @@ -156,13 +156,20 @@ def __init__(self, base_url, api_token): self.base_url = base_url self.api_token = api_token + def send_request(self, method, url, data=None): + headers = { + 'User-Agent': 'Python-urllib/3.8' + } + response = requests.request(method, url, data=data, headers=headers) + return response + def broadcast_tx(self, tx): tx_hex = tx broadcast_url = self.base_url + '?module=proxy&action=eth_sendRawTransaction' if self.api_token: broadcast_url += '&apikey=%s' % self.api_token - response = requests.post(broadcast_url, data={'hex': tx_hex}) + response = self.send_request('POST', broadcast_url, {'hex': tx_hex}) if 'error' in response.json(): logging.error("Etherscan returned an error: %s", response.json()['error']) raise BroadcastError(response.json()['error']) @@ -185,7 +192,7 @@ def get_balance(self, address): broadcast_url += '&tag=pending' if self.api_token: broadcast_url += '&apikey=%s' % self.api_token - response = requests.get(broadcast_url) + response = self.send_request('GET', broadcast_url) if int(response.status_code) == 200: if response.json().get('message', None) == 'NOTOK': raise BroadcastError(response.json().get('result', None)) @@ -204,7 +211,7 @@ def get_address_nonce(self, address): broadcast_url += '&tag=pending' # Valid tags are 'earliest', 'latest', and 'pending', the last of which includes both pending and committed transactions. if self.api_token: broadcast_url += '&apikey=%s' % self.api_token - response = requests.get(broadcast_url, ) + response = self.send_request('GET', broadcast_url) if int(response.status_code) == 200: if response.json().get('message', None) == 'NOTOK': raise BroadcastError(response.json().get('result', None)) From 34c30dc75b7e1ada7fd093f932b875b3e2846cd0 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 8 Dec 2021 13:21:47 +0100 Subject: [PATCH 45/48] feat(v3): prepare version --- cert_issuer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cert_issuer/__init__.py b/cert_issuer/__init__.py index f153c0d5..4eb28e38 100644 --- a/cert_issuer/__init__.py +++ b/cert_issuer/__init__.py @@ -1 +1 @@ -__version__ = '3.0.0b2' +__version__ = '3.0.0' From 2bdf62d5a744d1f7a3e58ea30a471daebbe99736 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 8 Dec 2021 14:14:43 +0100 Subject: [PATCH 46/48] feat(v3): bump cert-schema dependency --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c5703f79..7d5cc81f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cert-core>=3.0.0b1 -cert-schema>=3.0.0b4 +cert-schema>=3.0.0 merkletools==1.0.3 configargparse==0.12.0 glob2==0.6 From 8aa3290af3d8b79d806d252734efeeae69c2e677 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 8 Dec 2021 17:12:14 +0100 Subject: [PATCH 47/48] feat(v3): bump cert-schema dependency --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7d5cc81f..d4983cf5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cert-core>=3.0.0b1 -cert-schema>=3.0.0 +cert-schema>=3.0.2 merkletools==1.0.3 configargparse==0.12.0 glob2==0.6 From 7ce5a0a0bb5eae9f6871f8fd22747342b51a7e01 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Thu, 9 Dec 2021 13:47:45 +0100 Subject: [PATCH 48/48] docs(V3): update documentation --- README.md | 50 ++++++++++++++++++++++++++++------ docs/ethereum_configuration.md | 12 ++++++++ 2 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 docs/ethereum_configuration.md diff --git a/README.md b/README.md index fc2f0c9f..6b1783c1 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,12 @@ # cert-issuer The cert-issuer project issues blockchain certificates by creating a transaction from the issuing institution to the -recipient on the Bitcoin blockchain that includes the hash of the certificate itself. +recipient on the Bitcoin or Ethereum blockchains. That transaction includes the hash of the certificate itself. -Note: Work on Blockcerts v3 is underway. It will handle Blockcerts using the Verifiable Credentials standard, and sign them with MerkleProof2019. Thus, cert-issuer v3 will _not_ be backward compatible - v2 Blockcerts will not issue from v3 cert-issuer. +Blockcerts v3 is released. This new version of the standard leverages the [W3C Verifiable Credentials specification](https://www.w3.org/TR/vc-data-model/), and documents are signed with [MerkleProof2019 LD signature](https://w3c-ccg.github.io/lds-merkle-proof-2019/). Use of [DIDs (Decentralized Identifiers)](https://www.w3.org/TR/did-core/) is also possible to provide more cryptographic proof of the ownership of the issuing address. See [section](#working-with-dids) down below + +Cert-issuer v3 is _not_ backwards compatible and does not support Blockcerts v2 issuances. If you need to work with v2, you need to install cert-issuer v2 or use the [v2](https://github.com/blockchain-certificates/cert-issuer/tree/v2) branch of this repo. +You may expect little to no maintenance to the v2 code at this point. ## Web resources For development or testing using web requests, check out the documentation at [docs/web_resources.md](./docs/web_resources.md). @@ -124,7 +127,7 @@ Suppose the batch contains `n` certificates, and certificate `i` contains recipi The root of the Merkle tree, which is a 256-bit hash, is issued on the Bitcoin blockchain. The complete Bitcoin transaction outputs are described in 'Transaction structure'. -The Blockchain Certificate given to recipient `i` contains a [2017 Merkle Proof Signature Suite](https://w3c-ccg.github.io/lds-merkleproof2017/)-formatted signature, proving that certificate `i` is contained in the Merkle tree. +The Blockchain Certificate given to recipient `i` contains a [2019 Merkle Proof Signature Suite](https://w3c-ccg.github.io/lds-merkleproof2019/)-formatted proof, proving that certificate `i` is contained in the Merkle tree. ![](img/blockchain_certificate_components.png) @@ -145,7 +148,7 @@ These steps establish that the certificate has not been tampered with since it w ## Hashing a certificate -The Blockchain Certificate JSON contents without the `signature` node is the certificate that the issuer created. This is the value needed to hash for comparison against the receipt. Because there are no guarantees about ordering or formatting of JSON, first canonicalize the certificate (without the `signature`) against the JSON LD schema. This allows us to obtain a deterministic hash across platforms. +The Blockchain Certificate JSON contents without the `proof` node is the certificate that the issuer created. This is the value needed to hash for comparison against the receipt. Because there are no guarantees about ordering or formatting of JSON, first canonicalize the certificate (without the `proof`) against the JSON LD schema. This allows us to obtain a deterministic hash across platforms. The detailed steps are described in the [verification process](https://github.com/blockchain-certificates/cert-verifier-js#verification-process). @@ -188,7 +191,7 @@ These steps walk you through issuing in testnet and mainnet mode. Note that the ## Prerequisites -Decide which chain (Bitcoin or Ethereum) to issue to and follow the steps. The bitcoin chain is currently best supported by the Blockcerts libraries. Follow the steps for the chosen chain. +Decide which chain (Bitcoin or Ethereum) to issue to and follow the steps. Follow the steps for the chosen chain. ### Install cert-issuer @@ -208,10 +211,10 @@ python setup.py experimental --blockchain=ethereum See the docs here for helpful tips on creating / funding blockchain addresses: [docs/testnet_mainnet_addresses](./docs/testnet_mainnet_addresses.md) - ## Configuring cert-issuer -Edit your conf.ini file (the config file for this application). +Edit your conf.ini file (the config file for this application). See [here](./docs/ethereum_configuration.md) for more details on Ethereum configuration. +The private key for bitcoin should be the WIF format. ``` issuing_address = @@ -236,7 +239,38 @@ no_safe_mode Notes: - The `bitcoind` option is technically not required in `regtest` mode. `regtest` mode _only_ works with a local bitcoin node. The quick start in docker brushed over this detail by installing a regtest-configured bitcoin node in the docker container. - - The Ethereum option does not support a local (test)node currently. The issuer will broadcast the transaction via the Etherscan API. + - The Ethereum option does not support a local (test)node currently. The issuer will broadcast the transaction via the Etherscan API or an RPC of their choice. + +## Working with DIDs +To issue and verify a Blockcerts document bound to a DID you need to: +- generate a DID document referencing the public key source of the issuing address. The verification supports all the DID methods from the [DIF universal resolver](https://resolver.identity.foundation/), but it is recommended you provide your own resolver to the verification library. +- it is also expected that the DID document contains a `service` property configured similarly to as follows: + ``` + "service": [ + { + "id": "#service-1", + "type": "IssuerProfile", + "serviceEndpoint": "https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json" + } + ] + ``` +- reference the DID through the `issuer` property of the document to be issued as Blockcerts. Either directly as a string or as the `id` property of an object: + ``` + "issuer": "did:ion:EiA_Z6LQILbB2zj_eVrqfQ2xDm4HNqeJUw5Kj2Z7bFOOeQ", + ``` + or + ``` + "issuer": { + "id": "did:ion:EiA_Z6LQILbB2zj_eVrqfQ2xDm4HNqeJUw5Kj2Z7bFOOeQ", + ... /* more custom data here. Note that the data from the distant Issuer Profile has display preference in Blockcerts Verifier */ + } + ``` +- finally add to your `conf.ini` file the `verification_method` property pointing to the public key matching the issuing address: + ``` + verification_method=did:ion:EiA_Z6LQILbB2zj_eVrqfQ2xDm4HNqeJUw5Kj2Z7bFOOeQ#key-1 + ``` + +You may try to see the full example DID document by looking up `did:ion:EiA_Z6LQILbB2zj_eVrqfQ2xDm4HNqeJUw5Kj2Z7bFOOeQ` in the [DIF universal resolver](https://resolver.identity.foundation/). ## Issuing diff --git a/docs/ethereum_configuration.md b/docs/ethereum_configuration.md new file mode 100644 index 00000000..cddd038c --- /dev/null +++ b/docs/ethereum_configuration.md @@ -0,0 +1,12 @@ +# Issuing on the Ethereum Blockchain + +To issue on the Ethereum blockchain, you will to configure the following: + +## pk_issuer.txt +This should hold the Hex string of the BIP32 derived private key, generated from your own seed, prefixed by `0x`. + +## conf.ini +``` +issuing_address=0xYOUR_ADDRESS # matching with the private key above +chain=ethereum_ropsten # one of ['ethereum_ropsten', 'ethereum_mainnet'] +```