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/README.md b/README.md index 6ec7d44d..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,14 +211,17 @@ 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 = +# issuer URL / DID +verification_method = + chain= usb_name = @@ -233,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/cert_issuer/__init__.py b/cert_issuer/__init__.py index 41365263..4eb28e38 100644 --- a/cert_issuer/__init__.py +++ b/cert_issuer/__init__.py @@ -1 +1 @@ -__version__ = '2.0.29' +__version__ = '3.0.0' 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 c7166870..09a27a39 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/blockchain_handlers/ethereum/connectors.py b/cert_issuer/blockchain_handlers/ethereum/connectors.py index d19d0e97..8fe7f534 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://api-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,14 +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}, headers={'user-agent':'cert-issuer'}) - + 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']) @@ -186,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, headers={'user-agent':'cert-issuer'}) + 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)) @@ -205,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, headers={'user-agent':'cert-issuer'}) + 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)) diff --git a/cert_issuer/certificate_handlers.py b/cert_issuer/certificate_handlers.py index c1f050d0..11f14344 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) @@ -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)) @@ -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.verification_method, chain) for metadata in self.certificates_to_issue: proof = next(proof_generator) self.proof.append(self.certificate_handler.add_proof(metadata, proof)) @@ -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: @@ -120,7 +121,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.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 2bfc505e..8c554886 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/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 ad8921fd..a169d86c 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 merkletools 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, verification_method, 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": verification_method + } yield merkle_proof diff --git a/cert_issuer/models.py b/cert_issuer/models.py index ce6279a6..e3d5e526 100644 --- a/cert_issuer/models.py +++ b/cert_issuer/models.py @@ -1,12 +1,146 @@ +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 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__(':')) + +def validate_url (url): + if not is_valid_url (parsed_url): + raise ValueError('Invalid URL: {}'.format(url)) + pass + +def validate_type (certificate_type): + compulsory_types = ['VerifiableCredential', 'VerifiablePresentation'] + if not isinstance(certificate_type, list): + 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_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 + +def validate_issuer (certificate_issuer): + 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): + 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 + +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 + +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 + +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('`issuanceDate` 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): + 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): @@ -23,6 +157,15 @@ def set_certificates_in_batch(self, certificates_to_issue): class CertificateHandler(object): @abstractmethod def validate_certificate(self, certificate_metadata): + validate_type(certificate_metadata['type']) + validate_context(certificate_metadata['@context'], certificate_metadata['type']) + + if (certificate_metadata['type'][0] == 'VerifiableCredential'): + verify_credential(certificate_metadata) + + if (certificate_metadata['type'][0] == 'VerifiablePresentation'): + verify_presentation(certificate_metadata) + pass @abstractmethod 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/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'] +``` 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..37ab8c1d --- /dev/null +++ b/examples/data-testnet/unsigned_certificates/verifiable-credential.json @@ -0,0 +1,20 @@ +{ + "@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": "did:example:23adb1f712ebc6f1c276eba4dfa", + "issuanceDate": "2010-01-01T19:33:24Z", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "alumniOf": { + "id": "did:example:c276e12ec21ebfeb1f712ebc6f1" + } + } +} diff --git a/requirements.txt b/requirements.txt index e8979572..d4983cf5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,14 @@ -cert-core>=2.1.10 -cert-schema>=3.0.0a9 +cert-core>=3.0.0b1 +cert-schema>=3.0.2 merkletools==1.0.3 configargparse==0.12.0 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 jsonschema<3.0.0 +lds-merkle-proof-2019>=0.0.2 diff --git a/tests/test_certificate_handler.py b/tests/test_certificate_handler.py index 4975413c..1a82ac9c 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 = "http://example.com" + 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 = "http://example.com" + 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 = "http://example.com" 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,18 +162,18 @@ 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'} - 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() 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,8 +192,13 @@ def test_web_add_proof(self): 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 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_merkle_tree_generator.py b/tests/test_merkle_tree_generator.py index 20bd788d..426a138c 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(): @@ -39,26 +41,54 @@ 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', chain) + '8087c03e7b7bc9ca7b355de9d9d8165cc5c76307f337f0deb8a204d002c8e582', 'http://example.com', 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', - 'type': type, - 'chain': display_chain}]} - p3_expected = {'type': ['MerkleProof2017', 'Extension'], - 'merkleRoot': '0932f1d2e98219f7d7452801e2b64ebd9e5c005539db12d9b1ddabe7834d9044', - 'targetHash': '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce', - 'proof': [{'left': '4295f72eeb1e3507b8461e240e3b8d18c1e7bd2f1122b11fc9ec40a65894031a'}], - 'anchors': [{'sourceId': '8087c03e7b7bc9ca7b355de9d9d8165cc5c76307f337f0deb8a204d002c8e582', - 'type': type, - '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": "http://example.com" + } + + 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": "http://example.com" + } + self.assertEqual(p1, p1_expected) self.assertEqual(p3, p3_expected) 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) diff --git a/tests/v3_certificate_validation/test_integration_verify_credential.py b/tests/v3_certificate_validation/test_integration_verify_credential.py new file mode 100644 index 00000000..48acc234 --- /dev/null +++ b/tests/v3_certificate_validation/test_integration_verify_credential.py @@ -0,0 +1,173 @@ +import unittest +import mock +import copy + +from cert_issuer.certificate_handlers import CertificateBatchHandler, CertificateV3Handler +from cert_issuer.models import CertificateHandler + +credential_example = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.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 TestIssuanceBatchValidation (unittest.TestCase): + 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( + 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), '`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 + +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() 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() 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_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() 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..722c2067 --- /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): + candidate = ['VerifiableCredential'] + try: + validate_type(candidate) + except: + assert False + return + + assert True + + def test_validate_type_invalid_credential_type (self): + candidate = ['SomethingSomething'] + try: + validate_type(candidate) + except: + assert True + return + + assert False + + def test_validate_type_invalid_credential_shape (self): + candidate = 'VerifiableCredential' + try: + validate_type(candidate) + except: + assert True + return + + assert False + + def test_validate_type_invalid_credential_definition (self): + candidate = [] + try: + validate_type(candidate) + except: + assert True + return + + assert False + +if __name__ == '__main__': + unittest.main() 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() 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() 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() 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..9e651a69 --- /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): + candidate = ['VerifiablePresentation'] + try: + validate_type(candidate) + except: + assert False + return + + assert True + + def test_validate_type_invalid_presentation_type (self): + candidate = ['SomethingSomething'] + try: + validate_type(candidate) + except: + assert True + return + + assert False + + def test_validate_type_invalid_presentation_shape (self): + candidate = 'VerifiablePresentation' + try: + validate_type(candidate) + except: + assert True + return + + assert False + + def test_validate_type_invalid_presentation_definition (self): + candidate = [] + try: + validate_type(candidate) + except: + assert True + return + + assert False + +if __name__ == '__main__': + unittest.main() 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..137efdbf --- /dev/null +++ b/tests/v3_certificate_validation/test_unit_verify_credential.py @@ -0,0 +1,163 @@ +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" + } + } +} + +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() 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() 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