diff --git a/indy_client/cli/cli.py b/indy_client/cli/cli.py index 87ef7cf82..d6d4c8de1 100644 --- a/indy_client/cli/cli.py +++ b/indy_client/cli/cli.py @@ -62,7 +62,7 @@ from plenum.cli.constants import PROMPT_ENV_SEPARATOR, NO_ENV from plenum.cli.helper import getClientGrams from plenum.cli.phrase_word_completer import PhraseWordCompleter -from plenum.common.constants import NAME, VERSION, VERKEY, DATA, TXN_ID, FORCE +from plenum.common.constants import NAME, VERSION, VERKEY, DATA, TXN_ID, FORCE, RAW from plenum.common.exceptions import OperationError from plenum.common.signer_did import DidSigner from plenum.common.txn_util import createGenesisTxnFile @@ -672,11 +672,15 @@ def _getAttr(self, nym, raw, enc, hsh): def getAttrReply(reply, err, *args): if reply and reply[DATA]: - data = json.loads(reply[DATA]) - if data: - self.print( - "Found attribute {}" - .format(json.dumps(data))) + data_to_print = None + if RAW in reply: + data = json.loads(reply[DATA]) + if data: + data_to_print = json.dumps(data) + else: + data_to_print = reply[DATA] + if data_to_print: + self.print("Found attribute {}".format(data_to_print)) else: self.print("Attr not found") @@ -843,12 +847,9 @@ def _sendAttribAction(self, matchedVars): if not self.canMakeIndyRequest: return True nym = matchedVars.get('dest_id') - raw = matchedVars.get('raw') \ - if matchedVars.get('raw') else None - enc = ast.literal_eval(matchedVars.get('enc')) \ - if matchedVars.get('enc') else None - hsh = matchedVars.get('hash') \ - if matchedVars.get('hash') else None + raw = matchedVars.get('raw', None) + enc = matchedVars.get('enc', None) + hsh = matchedVars.get('hash', None) self._addAttribToNym(nym, raw, enc, hsh) return True @@ -859,12 +860,9 @@ def _sendGetAttrAction(self, matchedVars): if not self.canMakeIndyRequest: return True nym = matchedVars.get('dest_id') - raw = matchedVars.get('raw') \ - if matchedVars.get('raw') else None - enc = ast.literal_eval(matchedVars.get('enc')) \ - if matchedVars.get('enc') else None - hsh = matchedVars.get('hash') \ - if matchedVars.get('hash') else None + raw = matchedVars.get('raw', None) + enc = matchedVars.get('enc', None) + hsh = matchedVars.get('hash', None) self._getAttr(nym, raw, enc, hsh) return True diff --git a/indy_client/cli/command.py b/indy_client/cli/command.py index 19b640cb7..e5b665eec 100644 --- a/indy_client/cli/command.py +++ b/indy_client/cli/command.py @@ -40,7 +40,7 @@ sendAttribCmd = Command( id="send {attrib}".format(attrib=attribName), title="Adds attributes to existing DID", - usage="send {attrib} dest= [raw={{}}] [hash=] [enc: ]".format( + usage="send {attrib} dest= [raw={{}}] [hash=] [enc=]".format( attrib=attribName), examples='send {attrib} dest=33A18XMqWqTzDpLHXLR5nT raw={{"endpoint": "127.0.0.1:5555"}}'.format(attrib=attribName)) @@ -48,7 +48,7 @@ id="send {getAttr}".format( getAttr=getAttrName), title="Get ATTR from indy", - usage="send {getAttr} dest= raw=".format( + usage="send {getAttr} dest= [raw=] [hash=] [enc=]".format( getAttr=getAttrName), examples="send {getAttr} dest=33A18XMqWqTzDpLHXLR5nT raw=endpoint".format( getAttr=getAttrName)) diff --git a/indy_client/cli/constants.py b/indy_client/cli/constants.py index b58c1fedb..4ae0a9272 100644 --- a/indy_client/cli/constants.py +++ b/indy_client/cli/constants.py @@ -44,13 +44,13 @@ GET_ATTR_REG_EX = \ "(\s* (?Psend\s+{attrib}) " \ "\s+ dest=\s*(?P[A-Za-z0-9+=/]+) " \ - "\s+ raw=(?P[A-Za-z0-9+=/]+) \s*) ".format( + "\s+ ((raw=(?P[A-Za-z0-9+=/]+))|(hash=(?P[A-Fa-f0-9]+))|(enc=(?P[A-Za-z0-9+=/]+)) \s*) \s*) ".format( attrib=IndyTransactions.GET_ATTR.name) ADD_ATTRIB_REG_EX = \ "(\s* (?Psend\s+{attrib}) " \ "\s+ dest=\s*(?P[A-Za-z0-9+=/]+) " \ - "\s+ raw=(?P\{{\s*.*\}}) \s*) ".format( + "\s+ ((raw=(?P\{{\s*.*\}}))|(hash=(?P[A-Fa-f0-9]+))|(enc=(?P[A-Za-z0-9+=/]+))) \s*) ".format( attrib=IndyTransactions.ATTRIB.name) SEND_SCHEMA_REG_EX = "(\s*(?Psend\s+{schema})" \ diff --git a/indy_client/client/client.py b/indy_client/client/client.py index dc1963423..a346659a3 100644 --- a/indy_client/client/client.py +++ b/indy_client/client/client.py @@ -141,7 +141,7 @@ def prepare_for_state(self, result): if request_type == GET_NYM: return domain.prepare_get_nym_for_state(result) if request_type == GET_ATTR: - path, value, hashed_value, value_bytes = \ + attr_type, path, value, hashed_value, value_bytes = \ domain.prepare_get_attr_for_state(result) return path, value_bytes if request_type == GET_CLAIM_DEF: diff --git a/indy_client/client/wallet/attribute.py b/indy_client/client/wallet/attribute.py index 9308799bb..ac1a7ca55 100644 --- a/indy_client/client/wallet/attribute.py +++ b/indy_client/client/wallet/attribute.py @@ -1,7 +1,7 @@ from enum import unique, IntEnum from typing import Optional, TypeVar -from plenum.common.constants import TXN_TYPE, TARGET_NYM, RAW, ORIGIN, CURRENT_PROTOCOL_VERSION +from plenum.common.constants import TXN_TYPE, TARGET_NYM, RAW, ENC, HASH, ORIGIN, CURRENT_PROTOCOL_VERSION from indy_common.generates_request import GeneratesRequest from indy_common.constants import ATTRIB, GET_ATTR from indy_common.types import Request @@ -62,23 +62,27 @@ def __init__(self, self.encKey = encKey self.seqNo = seqNo - def _op(self): - op = { - TXN_TYPE: ATTRIB - } - if self.dest: - op[TARGET_NYM] = self.dest + def _op_fill_attr_type_and_data(self, op, data): if self.ledgerStore == LedgerStore.RAW: - op[RAW] = self.value + op[RAW] = data elif self.ledgerStore == LedgerStore.ENC: - raise NotImplementedError + op[ENC] = data elif self.ledgerStore == LedgerStore.HASH: - raise NotImplementedError + op[HASH] = data elif self.ledgerStore == LedgerStore.DONT: raise RuntimeError("This attribute cannot be stored externally") else: raise RuntimeError("Unknown ledgerStore: {}". format(self.ledgerStore)) + + def _op(self): + op = { + TXN_TYPE: ATTRIB + } + if self.dest: + op[TARGET_NYM] = self.dest + self._op_fill_attr_type_and_data(op, self.value) + return op def ledgerRequest(self): @@ -92,8 +96,8 @@ def _opForGet(self): op = { TARGET_NYM: self.dest, TXN_TYPE: GET_ATTR, - RAW: self.name } + self._op_fill_attr_type_and_data(op, self.name) if self.origin: op[ORIGIN] = self.origin return op diff --git a/indy_client/client/wallet/wallet.py b/indy_client/client/wallet/wallet.py index e6a92b76f..846a0e908 100644 --- a/indy_client/client/wallet/wallet.py +++ b/indy_client/client/wallet/wallet.py @@ -408,17 +408,16 @@ def getConnectionNames(self): def build_attrib(self, nym, raw=None, enc=None, hsh=None): assert int(bool(raw)) + int(bool(enc)) + int(bool(hsh)) == 1 if raw: - # l = LedgerStore.RAW + store = LedgerStore.RAW data = raw elif enc: - # l = LedgerStore.ENC + store = LedgerStore.ENC data = enc elif hsh: - # l = LedgerStore.HASH + store = LedgerStore.HASH data = hsh else: raise RuntimeError('One of raw, enc, or hash are required.') - # TODO looks like a possible error why we do not use `l` (see above)? return Attribute(randomString(5), data, self.defaultId, - dest=nym, ledgerStore=LedgerStore.RAW) + dest=nym, ledgerStore=store) diff --git a/indy_client/test/cli/test_send_attrib_validation.py b/indy_client/test/cli/test_send_attrib_validation.py index 220b98818..89ac44956 100644 --- a/indy_client/test/cli/test_send_attrib_validation.py +++ b/indy_client/test/cli/test_send_attrib_validation.py @@ -724,7 +724,6 @@ def testSendAttribFailsIfRawContainsNoAttrs( mapper=parameters, expect=ERROR, within=2) -@pytest.mark.skip(reason='INDY-71') def testSendAttribFailsIfRawIsBrokenJson( be, do, poolNodesStarted, trusteeCli): @@ -743,11 +742,15 @@ def testSendAttribFailsIfRawIsBrokenJson( } be(trusteeCli) + do('send ATTRIB dest={dest} raw={raw}', + mapper=parameters, expect=INVALID_SYNTAX, within=2) + + brokenJson = validJson.replace(':', '-') + parameters['raw'] = brokenJson do('send ATTRIB dest={dest} raw={raw}', mapper=parameters, expect=ERROR, within=2) -@pytest.mark.skip(reason='INDY-71') def testSendAttribFailsIfRawIsHex( be, do, poolNodesStarted, trusteeCli): @@ -761,10 +764,9 @@ def testSendAttribFailsIfRawIsHex( be(trusteeCli) do('send ATTRIB dest={dest} raw={raw}', - mapper=parameters, expect=ERROR, within=2) + mapper=parameters, expect=INVALID_SYNTAX, within=2) -@pytest.mark.skip(reason='INDY-71') def testSendAttribFailsIfRawIsHumanReadableText( be, do, poolNodesStarted, trusteeCli): @@ -778,10 +780,9 @@ def testSendAttribFailsIfRawIsHumanReadableText( be(trusteeCli) do('send ATTRIB dest={dest} raw={raw}', - mapper=parameters, expect=ERROR, within=2) + mapper=parameters, expect=INVALID_SYNTAX, within=2) -@pytest.mark.skip(reason='INDY-71') def testSendAttribFailsIfRawIsDecimalNumber( be, do, poolNodesStarted, trusteeCli): @@ -795,7 +796,7 @@ def testSendAttribFailsIfRawIsDecimalNumber( be(trusteeCli) do('send ATTRIB dest={dest} raw={raw}', - mapper=parameters, expect=ERROR, within=2) + mapper=parameters, expect=INVALID_SYNTAX, within=2) def testSendAttribHasInvalidSyntaxIfRawIsEmptyString( @@ -814,7 +815,6 @@ def testSendAttribHasInvalidSyntaxIfRawIsEmptyString( mapper=parameters, expect=INVALID_SYNTAX, within=2) -@pytest.mark.skip(reason='INDY-71') def testSendAttribSucceedsForHexSha256Hash( be, do, poolNodesStarted, trusteeCli): @@ -835,7 +835,6 @@ def testSendAttribSucceedsForHexSha256Hash( mapper=parameters, expect=ATTRIBUTE_ADDED, within=2) -@pytest.mark.skip(reason='INDY-71') def testSendAttribSucceedsForHexHashWithLettersInBothCases( be, do, poolNodesStarted, trusteeCli): @@ -844,14 +843,14 @@ def testSendAttribSucceedsForHexHashWithLettersInBothCases( parameters = { 'dest': uuidIdentifier, - 'hash': '6d4a333838d0ef96756cccC680AF2531075C512502Fb68c5503c63d93de859b3'} + 'hash': '6d4a333838d0ef96756cccC680AF2531075C512502Fb68c5503c63d93de859b3' + } be(trusteeCli) do('send ATTRIB dest={dest} hash={hash}', mapper=parameters, expect=ATTRIBUTE_ADDED, within=2) -@pytest.mark.skip(reason='INDY-71') def testSendAttribFailsForHashShorterThanSha256( be, do, poolNodesStarted, trusteeCli): @@ -868,7 +867,6 @@ def testSendAttribFailsForHashShorterThanSha256( mapper=parameters, expect=ERROR, within=2) -@pytest.mark.skip(reason='INDY-71') def testSendAttribFailsForHashLongerThanSha256( be, do, poolNodesStarted, trusteeCli): @@ -885,7 +883,6 @@ def testSendAttribFailsForHashLongerThanSha256( mapper=parameters, expect=ERROR, within=2) -@pytest.mark.skip(reason='INDY-71') def testSendAttribFailsForBase58Hash( be, do, poolNodesStarted, trusteeCli): @@ -905,10 +902,9 @@ def testSendAttribFailsForBase58Hash( be(trusteeCli) do('send ATTRIB dest={dest} hash={hash}', - mapper=parameters, expect=ERROR, within=2) + mapper=parameters, expect=INVALID_SYNTAX, within=2) -@pytest.mark.skip(reason='INDY-71') def testSendAttribFailsForBase64Hash( be, do, poolNodesStarted, trusteeCli): @@ -928,7 +924,7 @@ def testSendAttribFailsForBase64Hash( be(trusteeCli) do('send ATTRIB dest={dest} hash={hash}', - mapper=parameters, expect=ERROR, within=2) + mapper=parameters, expect=INVALID_SYNTAX, within=2) def testSendAttribHasInvalidSyntaxIfHashIsEmpty( @@ -947,7 +943,6 @@ def testSendAttribHasInvalidSyntaxIfHashIsEmpty( mapper=parameters, expect=INVALID_SYNTAX, within=2) -@pytest.mark.skip(reason='INDY-71') def testSendAttribSucceedsForNonEmptyEnc( be, do, poolNodesStarted, trusteeCli): diff --git a/indy_client/test/cli/test_send_get_attr.py b/indy_client/test/cli/test_send_get_attr.py index 123aa43dd..2e51fbee4 100644 --- a/indy_client/test/cli/test_send_get_attr.py +++ b/indy_client/test/cli/test_send_get_attr.py @@ -1,18 +1,29 @@ import pytest import json +from libnacl.secret import SecretBox +from hashlib import sha256 + from indy_client.test.cli.constants import INVALID_SYNTAX from indy_client.test.cli.helper import createUuidIdentifier, addNym + attrib_name = 'dateOfBirth' +secretBox = SecretBox() +enc_data = secretBox.encrypt(json.dumps({'name': 'Alice'}).encode()).hex() +hash_data = sha256(json.dumps({'name': 'Alice'}).encode()).hexdigest() + +FOUND_ATTRIBUTE = 'Found attribute' ATTRIBUTE_ADDED = 'Attribute added for nym {valid_dest}' -RETURNED_DATA = ['Found attribute', attrib_name, 'dayOfMonth', 'year', 'month'] +RETURNED_RAW_DATA = [FOUND_ATTRIBUTE, attrib_name, 'dayOfMonth', 'year', 'month'] +RETURNED_ENC_DATA = [FOUND_ATTRIBUTE, enc_data] +RETURNED_HASH_DATA = [FOUND_ATTRIBUTE, hash_data] ATTR_NOT_FOUND = 'Attr not found' @pytest.fixture(scope="module") -def send_attrib(be, do, poolNodesStarted, trusteeCli): +def send_raw_attrib(be, do, poolNodesStarted, trusteeCli): valid_identifier = createUuidIdentifier() invalid_identifier = createUuidIdentifier() @@ -38,50 +49,106 @@ def send_attrib(be, do, poolNodesStarted, trusteeCli): return parameters +@pytest.fixture(scope="module") +def send_enc_attrib(be, do, poolNodesStarted, trusteeCli): + + valid_identifier = createUuidIdentifier() + invalid_identifier = createUuidIdentifier() + addNym(be, do, trusteeCli, idr=valid_identifier) + + parameters = { + 'valid_dest': valid_identifier, + 'invalid_dest': invalid_identifier, + 'enc': enc_data + } + + be(trusteeCli) + do('send ATTRIB dest={valid_dest} enc={enc}', + mapper=parameters, expect=ATTRIBUTE_ADDED, within=2) + + return parameters + + +@pytest.fixture(scope="module") +def send_hash_attrib(be, do, poolNodesStarted, trusteeCli): + + valid_identifier = createUuidIdentifier() + invalid_identifier = createUuidIdentifier() + addNym(be, do, trusteeCli, idr=valid_identifier) + + parameters = { + 'valid_dest': valid_identifier, + 'invalid_dest': invalid_identifier, + 'hash': hash_data + } + + be(trusteeCli) + do('send ATTRIB dest={valid_dest} hash={hash}', + mapper=parameters, expect=ATTRIBUTE_ADDED, within=2) + + return parameters + + def test_send_get_attr_succeeds_for_existing_uuid_dest( - be, do, poolNodesStarted, trusteeCli, send_attrib): + be, do, poolNodesStarted, trusteeCli, send_raw_attrib): be(trusteeCli) do('send GET_ATTR dest={valid_dest} raw={attrib_name}', - mapper=send_attrib, expect=RETURNED_DATA, within=2) + mapper=send_raw_attrib, expect=RETURNED_RAW_DATA, within=2) def test_send_get_attr_fails_for_invalid_uuid_dest( - be, do, poolNodesStarted, trusteeCli, send_attrib): + be, do, poolNodesStarted, trusteeCli, send_raw_attrib): do('send GET_ATTR dest={invalid_dest} raw={attrib_name}', - mapper=send_attrib, expect=ATTR_NOT_FOUND, within=2) + mapper=send_raw_attrib, expect=ATTR_NOT_FOUND, within=2) def test_send_get_attr_fails_for_nonexistent_uuid_dest( - be, do, poolNodesStarted, trusteeCli, send_attrib): + be, do, poolNodesStarted, trusteeCli, send_raw_attrib): with pytest.raises(AssertionError) as excinfo: do('send GET_ATTR dest=this_is_not_valid raw={attrib_name}', - mapper=send_attrib, expect=ATTR_NOT_FOUND, within=2) + mapper=send_raw_attrib, expect=ATTR_NOT_FOUND, within=2) assert(INVALID_SYNTAX in str(excinfo.value)) def test_send_get_attr_fails_for_invalid_attrib( - be, do, poolNodesStarted, trusteeCli, send_attrib): + be, do, poolNodesStarted, trusteeCli, send_raw_attrib): do('send GET_ATTR dest={valid_dest} raw=badname', - mapper=send_attrib, expect=ATTR_NOT_FOUND, within=2) + mapper=send_raw_attrib, expect=ATTR_NOT_FOUND, within=2) def test_send_get_attr_fails_with_missing_dest( - be, do, poolNodesStarted, trusteeCli, send_attrib): + be, do, poolNodesStarted, trusteeCli, send_raw_attrib): with pytest.raises(AssertionError) as excinfo: do('send GET_ATTR raw={attrib_name}', - mapper=send_attrib, expect=ATTR_NOT_FOUND, within=2) + mapper=send_raw_attrib, expect=ATTR_NOT_FOUND, within=2) assert(INVALID_SYNTAX in str(excinfo.value)) def test_send_get_attr_fails_with_missing_attrib( - be, do, poolNodesStarted, trusteeCli, send_attrib): + be, do, poolNodesStarted, trusteeCli, send_raw_attrib): with pytest.raises(AssertionError) as excinfo: do('send GET_ATTR dest={valid_dest}', - mapper=send_attrib, expect=ATTR_NOT_FOUND, within=2) + mapper=send_raw_attrib, expect=ATTR_NOT_FOUND, within=2) assert(INVALID_SYNTAX in str(excinfo.value)) + + +def test_send_get_attr_enc_succeeds_for_existing_uuid_dest( + be, do, poolNodesStarted, trusteeCli, send_enc_attrib): + + be(trusteeCli) + do('send GET_ATTR dest={valid_dest} enc={enc}', + mapper=send_enc_attrib, expect=RETURNED_ENC_DATA, within=2) + + +def test_send_get_attr_hash_succeeds_for_existing_uuid_dest( + be, do, poolNodesStarted, trusteeCli, send_hash_attrib): + + be(trusteeCli) + do('send GET_ATTR dest={valid_dest} hash={hash}', + mapper=send_hash_attrib, expect=RETURNED_HASH_DATA, within=2) diff --git a/indy_common/state/domain.py b/indy_common/state/domain.py index f94116cb7..728b05745 100644 --- a/indy_common/state/domain.py +++ b/indy_common/state/domain.py @@ -21,8 +21,8 @@ def make_state_path_for_nym(did) -> bytes: return sha256(did.encode()).digest() -def make_state_path_for_attr(did, attr_name) -> bytes: - nameHash = sha256(attr_name.encode()).hexdigest() +def make_state_path_for_attr(did, attr_name, attr_is_hash=False) -> bytes: + nameHash = sha256(attr_name.encode()).hexdigest() if not attr_is_hash else attr_name return "{DID}:{MARKER}:{ATTR_NAME}"\ .format(DID=did, MARKER=MARKER_ATTR, @@ -75,13 +75,13 @@ def prepare_attr_for_state(txn): """ assert txn[TXN_TYPE] in {ATTRIB, GET_ATTR} nym = txn[TARGET_NYM] - attr_key, value = parse_attr_txn(txn) + attr_type, attr_key, value = parse_attr_txn(txn) hashed_value = hash_of(value) if value else '' seq_no = txn[f.SEQ_NO.nm] txn_time = txn[TXN_TIME] value_bytes = encode_state_value(hashed_value, seq_no, txn_time) - path = make_state_path_for_attr(nym, attr_key) - return path, value, hashed_value, value_bytes + path = make_state_path_for_attr(nym, attr_key, attr_type == HASH) + return attr_type, path, value, hashed_value, value_bytes def prepare_claim_def_for_state(txn): @@ -181,11 +181,11 @@ def parse_attr_txn(txn): re_raw = attrib_raw_data_serializer.serialize(data, toBytes=False) key, _ = data.popitem() - return key, re_raw + return attr_type, key, re_raw if attr_type == ENC: - return hash_of(attr), attr + return attr_type, attr, attr if attr_type == HASH: - return attr, None + return attr_type, attr, None def prepare_get_attr_for_state(txn): @@ -201,8 +201,9 @@ def prepare_get_attr_for_state(txn): if attr_type == ENC: attr_key = hash_of(attr_key) - path = make_state_path_for_attr(nym, attr_key) - return path, None, None, None + path = make_state_path_for_attr(nym, attr_key, + attr_type == HASH or attr_type == ENC) + return attr_type, path, None, None, None def _extract_attr_typed_value(txn): diff --git a/indy_common/test/types/test_attrib.py b/indy_common/test/types/test_attrib.py index 6417d1aa9..5b7ec2aaf 100644 --- a/indy_common/test/types/test_attrib.py +++ b/indy_common/test/types/test_attrib.py @@ -1,8 +1,10 @@ -import itertools +import json import pytest +from libnacl.secret import SecretBox + from plenum.common.constants import TARGET_NYM, RAW, ENC, HASH -from indy_common.constants import TXN_TYPE, allOpKeys, ATTRIB, GET_ATTR, \ +from indy_common.constants import TXN_TYPE, ATTRIB, GET_ATTR, \ DATA, GET_NYM, reqOpKeys, GET_TXNS, GET_SCHEMA, GET_CLAIM_DEF, ACTION, \ NODE_UPGRADE, COMPLETE, FAIL, CONFIG_LEDGER_ID, POOL_UPGRADE, POOL_CONFIG, \ IN_PROGRESS, DISCLO, ATTR_NAMES, REVOCATION, SCHEMA, ENDPOINT, CLAIM_DEF, \ @@ -14,6 +16,8 @@ validator = ClientAttribOperation() VALID_TARGET_NYM = 'a' * 43 +VALID_HASH = '6d4a333838d0ef96756cccC680AF2531075C512502Fb68c5503c63d93de859b3' +assert len(VALID_HASH) == 64 def test_attrib_with_enc_raw_hash_at_same_time_fails(): @@ -22,7 +26,7 @@ def test_attrib_with_enc_raw_hash_at_same_time_fails(): TARGET_NYM: VALID_TARGET_NYM, RAW: '{}', ENC: 'foo', - HASH: 'bar' + HASH: VALID_HASH } with pytest.raises(TypeError) as ex_info: validator.validate(msg) @@ -170,3 +174,113 @@ def test_attrib_with_raw_having_endpoint_ha_with_invalid_ip_address_fails(): ex_info.match( "validation error \[ClientAttribOperation\]: invalid endpoint address " "\(ha=256.8.8.8:9700\)") + + +def test_attrib_with_valid_hash_passes(): + msg = { + TXN_TYPE: ATTRIB, + TARGET_NYM: VALID_TARGET_NYM, + HASH: VALID_HASH + } + validator.validate(msg) + + +def test_attrib_with_shorter_hash_fails(): + invalid_hash = VALID_HASH[:-1] + msg = { + TXN_TYPE: ATTRIB, + TARGET_NYM: VALID_TARGET_NYM, + HASH: invalid_hash + } + with pytest.raises(TypeError) as ex_info: + validator.validate(msg) + ex_info.match("validation error \[ClientAttribOperation\]: not a valid hash " + "\(needs to be in hex too\) \({}={}\)".format(HASH, invalid_hash)) + + +def test_attrib_with_longer_hash_fails(): + invalid_hash = VALID_HASH + 'a' + msg = { + TXN_TYPE: ATTRIB, + TARGET_NYM: VALID_TARGET_NYM, + HASH: invalid_hash + } + with pytest.raises(TypeError) as ex_info: + validator.validate(msg) + ex_info.match("validation error \[ClientAttribOperation\]: not a valid hash " + "\(needs to be in hex too\) \({}={}\)".format(HASH, invalid_hash)) + + +def test_attrib_with_invalid_hash_fails(): + idx = 10 + invalid_hash = VALID_HASH[:idx] + 'X' + VALID_HASH[idx + 1:] + assert len(invalid_hash) == 64 + msg = { + TXN_TYPE: ATTRIB, + TARGET_NYM: VALID_TARGET_NYM, + HASH: invalid_hash + } + with pytest.raises(TypeError) as ex_info: + validator.validate(msg) + ex_info.match("validation error \[ClientAttribOperation\]: not a valid hash " + "\(needs to be in hex too\) \({}={}\)".format(HASH, invalid_hash)) + + +def test_attrib_with_empty_hash_fails(): + empty_hash = '' + msg = { + TXN_TYPE: ATTRIB, + TARGET_NYM: VALID_TARGET_NYM, + HASH: empty_hash + } + with pytest.raises(TypeError) as ex_info: + validator.validate(msg) + ex_info.match("validation error \[ClientAttribOperation\]: not a valid hash " + "\(needs to be in hex too\) \({}={}\)".format(HASH, empty_hash)) + + empty_hash = None + msg = { + TXN_TYPE: ATTRIB, + TARGET_NYM: VALID_TARGET_NYM, + HASH: empty_hash + } + with pytest.raises(TypeError) as ex_info: + validator.validate(msg) + ex_info.match("validation error \[ClientAttribOperation\]: expected types " + "'str', got 'NoneType' \({}={}\)".format(HASH, empty_hash)) + + +def test_attrib_with_enc_passes(): + secretBox = SecretBox() + enc_data = secretBox.encrypt(json.dumps({'name': 'Alice'}).encode()).hex() + + msg = { + TXN_TYPE: ATTRIB, + TARGET_NYM: VALID_TARGET_NYM, + ENC: enc_data + } + validator.validate(msg) + + +def test_attrib_with_empty_enc_fails(): + empty_enc = '' + msg = { + TXN_TYPE: ATTRIB, + TARGET_NYM: VALID_TARGET_NYM, + ENC: empty_enc + } + with pytest.raises(TypeError) as ex_info: + validator.validate(msg) + ex_info.match("validation error \[ClientAttribOperation\]: " + "empty string \({}={}\)".format(ENC, empty_enc)) + + empty_enc = None + msg = { + TXN_TYPE: ATTRIB, + TARGET_NYM: VALID_TARGET_NYM, + ENC: empty_enc + } + with pytest.raises(TypeError) as ex_info: + validator.validate(msg) + ex_info.match("validation error \[ClientAttribOperation\]: expected types " + "'str', got 'NoneType' \({}={}\)".format(ENC, empty_enc)) diff --git a/indy_common/test/types/test_attrib_schema.py b/indy_common/test/types/test_attrib_schema.py index dfd4a25e2..05bbef029 100644 --- a/indy_common/test/types/test_attrib_schema.py +++ b/indy_common/test/types/test_attrib_schema.py @@ -1,7 +1,8 @@ import pytest from indy_common.types import ClientAttribOperation from collections import OrderedDict -from plenum.common.messages.fields import ConstantField, LimitedLengthStringField, IdentifierField, JsonField +from plenum.common.messages.fields import ConstantField, LimitedLengthStringField, IdentifierField, \ + JsonField, Sha256HexField EXPECTED_ORDERED_FIELDS = OrderedDict([ @@ -9,7 +10,7 @@ ("dest", IdentifierField), ("raw", JsonField), ('enc', LimitedLengthStringField), - ('hash', LimitedLengthStringField), + ('hash', Sha256HexField) ]) diff --git a/indy_common/test/types/test_get_attrib_schema.py b/indy_common/test/types/test_get_attrib_schema.py index dc0390bf2..aee498db2 100644 --- a/indy_common/test/types/test_get_attrib_schema.py +++ b/indy_common/test/types/test_get_attrib_schema.py @@ -1,13 +1,15 @@ import pytest from indy_common.types import ClientGetAttribOperation from collections import OrderedDict -from plenum.common.messages.fields import ConstantField, LimitedLengthStringField, IdentifierField +from plenum.common.messages.fields import ConstantField, LimitedLengthStringField, IdentifierField, Sha256HexField EXPECTED_ORDERED_FIELDS = OrderedDict([ ("type", ConstantField), ("dest", IdentifierField), ("raw", LimitedLengthStringField), + ('enc', LimitedLengthStringField), + ('hash', Sha256HexField) ]) diff --git a/indy_common/types.py b/indy_common/types.py index cbe52e0ed..4b8f4f02f 100644 --- a/indy_common/types.py +++ b/indy_common/types.py @@ -104,15 +104,15 @@ class ClientAttribOperation(MessageValidator): (TARGET_NYM, IdentifierField(optional=True)), (RAW, JsonField(max_length=JSON_FIELD_LIMIT, optional=True)), (ENC, LimitedLengthStringField(max_length=ENC_FIELD_LIMIT, optional=True)), - (HASH, LimitedLengthStringField(max_length=HASH_FIELD_LIMIT, optional=True)), + (HASH, Sha256HexField(optional=True)), ) def _validate_message(self, msg): - self.__validate_field_set(msg) + self._validate_field_set(msg) if RAW in msg: self.__validate_raw_field(msg[RAW]) - def __validate_field_set(self, msg): + def _validate_field_set(self, msg): fields_n = sum(1 for f in (RAW, ENC, HASH) if f in msg) if fields_n == 0: self._raise_missed_fields(RAW, ENC, HASH) @@ -156,13 +156,18 @@ def __validate_endpoint_ha_field(self, endpoint): 'invalid endpoint port') -class ClientGetAttribOperation(MessageValidator): +class ClientGetAttribOperation(ClientAttribOperation): schema = ( (TXN_TYPE, ConstantField(GET_ATTR)), (TARGET_NYM, IdentifierField(optional=True)), - (RAW, LimitedLengthStringField(max_length=RAW_FIELD_LIMIT)), + (RAW, LimitedLengthStringField(max_length=RAW_FIELD_LIMIT, optional=True)), + (ENC, LimitedLengthStringField(max_length=ENC_FIELD_LIMIT, optional=True)), + (HASH, Sha256HexField(optional=True)), ) + def _validate_message(self, msg): + self._validate_field_set(msg) + class ClientClaimDefSubmitOperation(MessageValidator): schema = ( diff --git a/indy_node/server/domain_req_handler.py b/indy_node/server/domain_req_handler.py index 67d4841aa..62f6b689b 100644 --- a/indy_node/server/domain_req_handler.py +++ b/indy_node/server/domain_req_handler.py @@ -99,13 +99,6 @@ def _doStaticValidationAttrib(self, identifier, reqId, operation): '{} should have one and only one of ' '{}, {}, {}' .format(ATTRIB, RAW, ENC, HASH)) - # TODO: This is not static validation as it involves state - if not (not operation.get(TARGET_NYM) or - self.hasNym(operation[TARGET_NYM], isCommitted=False)): - raise InvalidClientRequest(identifier, reqId, - '{} should be added before adding ' - 'attribute for it'. - format(TARGET_NYM)) def validate(self, req: Request, config=None): op = req.operation @@ -330,14 +323,15 @@ def handleGetAttrsReq(self, request: Request): .format(ATTRIB, RAW, ENC, HASH)) nym = request.operation[TARGET_NYM] if RAW in request.operation: - attr_key = request.operation[RAW] + attr_type = RAW elif ENC in request.operation: # If attribute is encrypted, it will be queried by its hash - attr_key = request.operation[ENC] + attr_type = ENC else: - attr_key = request.operation[HASH] + attr_type = HASH + attr_key = request.operation[attr_type] value, lastSeqNo, lastUpdateTime, proof = \ - self.getAttr(did=nym, key=attr_key) + self.getAttr(did=nym, key=attr_key, attr_type=attr_type) attr = None if value is not None: if HASH in request.operation: @@ -374,9 +368,10 @@ def _addAttr(self, txn) -> None: the trie stores a blank value for the key did+hash """ assert txn[TXN_TYPE] == ATTRIB - path, value, hashed_value, value_bytes = domain.prepare_attr_for_state(txn) + attr_type, path, value, hashed_value, value_bytes = domain.prepare_attr_for_state(txn) self.state.set(path, value_bytes) - self.attributeStore.set(hashed_value, value) + if attr_type != HASH: + self.attributeStore.set(hashed_value, value) def _addSchema(self, txn) -> None: assert txn[TXN_TYPE] == SCHEMA @@ -391,16 +386,17 @@ def _addClaimDef(self, txn) -> None: def getAttr(self, did: str, key: str, + attr_type, isCommitted=True) -> (str, int, int, list): assert did is not None assert key is not None - path = domain.make_state_path_for_attr(did, key) + path = domain.make_state_path_for_attr(did, key, attr_type == HASH) try: hashed_val, lastSeqNo, lastUpdateTime, proof = \ self.lookup(path, isCommitted) except KeyError: return None, None, None, None - if not hashed_val: + if not hashed_val or hashed_val == '': # Its a HASH attribute return hashed_val, lastSeqNo, lastUpdateTime, proof else: @@ -463,13 +459,8 @@ def transform_attrib_for_ledger(txn): in the ledger but only the hash of it. """ txn = deepcopy(txn) - attr_key, value = domain.parse_attr_txn(txn) - hashed_val = domain.hash_of(value) if value else '' - - if RAW in txn: - txn[RAW] = hashed_val - elif ENC in txn: - txn[ENC] = hashed_val - elif HASH in txn: - txn[HASH] = txn[HASH] + attr_type, _, value = domain.parse_attr_txn(txn) + if attr_type in [RAW, ENC]: + txn[attr_type] = domain.hash_of(value) if value else '' + return txn diff --git a/setup.py b/setup.py index f2be0c81c..a8dd14988 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ data_files=[( (BASE_DIR, ['data/nssm_original.exe']) )], - install_requires=['indy-plenum-dev==1.2.231', + install_requires=['indy-plenum-dev==1.2.233', 'indy-anoncreds-dev==1.0.32', 'python-dateutil', 'timeout-decorator'],