Beispiel #1
0
def load_signingpair(path: str) -> RetVal:
	'''Instantiates a signing pair from a file'''
	if not path:
		return RetVal(BadParameterValue, 'path may not be empty')
	
	if not os.path.exists(path):
		return RetVal(ResourceNotFound, '%s exists' % path)
	
	indata = None
	try:
		with open(path, "r") as fhandle:
			indata = json.load(fhandle)
	
	except Exception as e:
		return RetVal(ExceptionThrown, e)
	
	if not isinstance(indata, dict):
		return RetVal(BadData, 'File does not contain an Anselus JSON signing pair')

	try:
		jsonschema.validate(indata, __signing_pair_schema)
	except jsonschema.ValidationError:
		return RetVal(BadData, "file data does not validate")
	except jsonschema.SchemaError:
		return RetVal(InternalError, "BUG: invalid SigningPair schema")

	public_key = CryptoString(indata['VerificationKey'])
	private_key = CryptoString(indata['SigningKey'])
	if not public_key.is_valid() or not private_key.is_valid():
		return RetVal(BadData, 'Failure to base85 decode key data')
	
	return RetVal().set_value('keypair', SigningPair(public_key, private_key))
Beispiel #2
0
    def verify_signature(self, verify_key: CryptoString,
                         sigtype: str) -> RetVal:
        '''Verifies a signature, given a verification key'''

        if not verify_key.is_valid():
            return RetVal(BadParameterValue, 'bad verify key')

        sig_names = [x['name'] for x in self.signature_info]
        if sigtype not in sig_names:
            return RetVal(BadParameterValue, 'bad signature type')

        if verify_key.prefix != 'ED25519':
            return RetVal(UnsupportedEncryptionType, verify_key.prefix)

        if sigtype in self.signatures and not self.signatures[sigtype]:
            return RetVal(NotCompliant, 'empty signature ' + sigtype)

        sig = CryptoString()
        status = sig.set(self.signatures[sigtype])
        if status.error():
            return status

        try:
            vkey = nacl.signing.VerifyKey(verify_key.raw_data())
        except Exception as e:
            return RetVal(ExceptionThrown, e)

        try:
            data = self.make_bytestring(sig_names.index(sigtype))
            vkey.verify(data, sig.raw_data())
        except nacl.exceptions.BadSignatureError:
            return RetVal(InvalidKeycard)

        return RetVal()
Beispiel #3
0
    def verify_hash(self) -> RetVal:
        '''Checks that the entry's actual hash matches that in the hash field'''
        current_hash = CryptoString(self.hash)
        if not current_hash.is_valid():
            return RetVal(InvalidHash,
                          f"{self.hash} is not a valid CryptoString")

        status = self.get_hash(current_hash.prefix)
        if status.error():
            return status

        return RetVal()
Beispiel #4
0
    def sign(self, signing_key: CryptoString, sigtype: str) -> RetVal:
        '''Adds a signature to the  Note that for any change in the keycard fields, this 
		call must be made afterward. Note that successive signatures are deleted, such that 
		updating a User signature will delete the Organization signature which depends on it. The 
		sigtype must be Custody, User, or Organization, and the type is case-sensitive.'''
        if not signing_key.is_valid():
            return RetVal(BadParameterValue, 'signing key')

        if signing_key.prefix != 'ED25519':
            return RetVal(UnsupportedEncryptionType, signing_key.prefix)

        sig_names = [x['name'] for x in self.signature_info]
        if sigtype not in sig_names:
            return RetVal(BadParameterValue, 'sigtype')

        key = nacl.signing.SigningKey(signing_key.raw_data())

        # Clear all signatures which follow the current one. This expects that the signature_info
        # field lists the signatures in the order that they are required to appear.
        clear_sig = False
        sigtype_index = 0

        # We really do need to use an index here instead of just an iterator. Sheesh.
        for i in range(len(sig_names)):  # pylint: disable=consider-using-enumerate
            name = sig_names[i]
            if name == sigtype:
                clear_sig = True
                sigtype_index = i

            if clear_sig:
                self.signatures[name] = ''

        data = self.make_bytestring(sigtype_index + 1)
        signed = key.sign(data, Base85Encoder)
        self.signatures[sigtype] = 'ED25519:' + signed.signature.decode()
        return RetVal()
Beispiel #5
0
def test_addentry_usercard():
    '''Tests the ADDENTRY and USERCARD command processes'''
    dbconn = setup_test()
    dbdata = init_server(dbconn)

    conn = ServerConnection()
    assert conn.connect('localhost',
                        2001), "Connection to server at localhost:2001 failed"

    # password is 'SandstoneAgendaTricycle'
    pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \
       'dcCYkJLok65qussSyhN5TTZP+OTgzEI'
    devid = '22222222-2222-2222-2222-222222222222'
    devpair = EncryptionPair(
        CryptoString(r'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z'),
        CryptoString(r'CURVE25519:W30{oJ?w~NBbj{F8Ag4~<bcWy6_uQ{i{X?NDq4^l'))

    dbdata['pwhash'] = pwhash
    dbdata['devid'] = devid
    dbdata['devpair'] = devpair

    regcode_admin(dbdata, conn)
    login_admin(dbdata, conn)

    # Test setup is complete. Create a test keycard and do ADDENTRY

    # 1) Client sends the `ADDENTRY` command, attaching the entry data between the
    #    `----- BEGIN USER KEYCARD -----` header and the `----- END USER KEYCARD -----` footer.
    # 2) The server then checks compliance of the entry data. Assuming that it complies, the server
    #    generates a cryptographic signature and responds with `100 CONTINUE`, returning the
    #    signature, the hash of the data, and the hash of the previous entry in the database.
    # 3) The client verifies the signature against the organization’s verification key. This has
    #    the added benefit of ensuring that none of the fields were altered by the server and that
    #    the signature is valid.
    # 4) The client appends the hash from the previous entry as the `Previous-Hash` field
    # 5) The client verifies the hash value for the entry from the server and sets the `Hash` field
    # 6) The client signs the entry as the `User-Signature` field and then uploads the result to
    #    the server.
    # 7) Once uploaded, the server validates the `Hash` and `User-Signature` fields, and,
    #    assuming that all is well, adds it to the keycard database and returns `200 OK`.
    #
    # Once that monster of a process completes, the card should be successfully added, but we're not
    # done yet. From there, create a new user entry, chain it to the first, and upload it. This will
    # test the chaining code in the AddEntry command processing.
    #
    # Finally, once that entire mess is complete, send a USERCARD command and verify what is
    # returned against the data set up so far. It doesn't seem like piggybacking a test for a
    # different command off another is the right thing to do, but in this case, it's the perfect
    # setup to confirm that a user's entire keycard is handled properly.

    usercard = keycard.UserEntry()
    usercard.set_fields({
        'Name':
        'Test Administrator',
        'Workspace-ID':
        dbdata['admin_wid'],
        'User-ID':
        'admin',
        'Domain':
        'example.com',
        'Contact-Request-Verification-Key':
        'ED25519:d0-oQb;{QxwnO{=!|^62+E=UYk2Y3mr2?XKScF4D',
        'Contact-Request-Encryption-Key':
        'CURVE25519:yBZ0{1fE9{2<b~#i^R+JT-yh-y5M(Wyw_)}_SZOn',
        'Public-Encryption-Key':
        'CURVE25519:_`UC|vltn_%P5}~vwV^)oY){#uvQSSy(dOD_l(yE'
    })

    conn.send_message({
        'Action': "ADDENTRY",
        'Data': {
            'Base-Entry': usercard.make_bytestring(0).decode()
        }
    })

    response = conn.read_response(server_response)
    assert response['Code'] == 100 and \
     response['Status'] == 'CONTINUE' and \
     'Organization-Signature' in response['Data'] and \
     'Hash' in response['Data'] and \
     'Previous-Hash' in response['Data'], 'test_addentry(): server did return all needed fields'

    usercard.signatures['Organization'] = response['Data'][
        'Organization-Signature']

    # A regular client will check the entry cache, pull updates to the org card, and get the
    # verification key. Because this is just an integration test, we skip all that and just use
    # the known verification key from earlier in the test.
    status = usercard.verify_signature(CryptoString(dbdata['ovkey']),
                                       'Organization')
    assert not status.error(
    ), f"test_addentry(): org signature didn't verify: {status.info()}"

    assert response['Data']['Previous-Hash'] == dbdata['second_org_entry'].hash, \
     "test_addentry(): server did not attach correct Previous Hash to root user entry"

    usercard.prev_hash = response['Data']['Previous-Hash']
    usercard.hash = response['Data']['Hash']
    status = usercard.verify_hash()
    assert not status.error(
    ), f"test_addentry(): hash didn't verify: {status.info()}"

    # User sign and verify
    skey = CryptoString('ED25519:ip52{ps^jH)t$k-9bc_RzkegpIW?}FFe~BX&<V}9')
    assert skey.is_valid(
    ), "test_addentry(): failed to set user's cr signing key"
    status = usercard.sign(skey, 'User')
    assert not status.error(), "test_addentry(): failed to user sign"

    vkey = CryptoString('ED25519:d0-oQb;{QxwnO{=!|^62+E=UYk2Y3mr2?XKScF4D')
    assert vkey.is_valid(
    ), "test_addentry(): failed to set user's cr verification key"
    status = usercard.verify_signature(vkey, 'User')

    status = usercard.is_compliant()
    assert not status.error(
    ), f"test_addentry(): compliance error: {str(status)}"

    conn.send_message({
        'Action': "ADDENTRY",
        'Data': {
            'User-Signature': usercard.signatures['User']
        }
    })

    response = conn.read_response(server_response)
    assert response['Code'] == 200 and \
     response['Status'] == 'OK', f"test_addentry(): final upload server error {response}"

    # Now to test ADDENTRY with a chained entry. Hold on tight!

    # Submit the chained entry
    newkeys = usercard.chain(skey, True)
    assert not newkeys.error(
    ), f"test_addentry(): new entry failed to chain: {newkeys.info()}"

    second_user_entry = newkeys['entry']
    conn.send_message({
        'Action': "ADDENTRY",
        'Data': {
            'Base-Entry': second_user_entry.make_bytestring(1).decode()
        }
    })

    response = conn.read_response(server_response)
    assert response['Code'] == 100 and \
     response['Status'] == 'CONTINUE' and \
     'Organization-Signature' in response['Data'] and \
     'Hash' in response['Data'] and \
     'Previous-Hash' in response['Data'], 'test_addentry(): server did return all needed fields'

    second_user_entry.signatures['Organization'] = response['Data'][
        'Organization-Signature']

    # Verify the server's signature, add and verify the hashes, sign, and upload
    status = second_user_entry.verify_signature(CryptoString(dbdata['ovkey']),
                                                'Organization')
    assert not status.error(
    ), f"test_addentry(): org signature didn't verify: {status.info()}"

    second_user_entry.prev_hash = response['Data']['Previous-Hash']
    assert response['Data']['Previous-Hash'] == usercard.hash, \
     "Server did not return correct chained user entry hash"
    second_user_entry.hash = response['Data']['Hash']
    status = second_user_entry.verify_hash()
    assert not status.error(
    ), f"test_addentry(): hash didn't verify: {status.info()}"

    skey = CryptoString(newkeys['crsign.private'])
    assert skey.is_valid(), "test_addentry(): failed to set user signing key"
    status = second_user_entry.sign(skey, 'User')
    assert not status.error(), "test_addentry(): failed to user sign"

    vkey = CryptoString(newkeys['crsign.public'])
    assert vkey.is_valid(
    ), "test_addentry(): failed to set user verification key"
    status = second_user_entry.verify_signature(vkey, 'User')

    status = second_user_entry.is_compliant()
    assert not status.error(
    ), f"test_addentry(): compliance error: {str(status)}"

    conn.send_message({
        'Action': "ADDENTRY",
        'Data': {
            'User-Signature': second_user_entry.signatures['User']
        }
    })

    response = conn.read_response(server_response)
    assert response['Code'] == 200 and \
     response['Status'] == 'OK', f"test_addentry(): final upload server error {response}"

    conn.send_message({
        'Action': "USERCARD",
        'Data': {
            'Owner': 'admin/example.com',
            'Start-Index': '1'
        }
    })

    response = conn.read_response(server_response)
    assert response['Code'] == 104 and response['Status'] == 'TRANSFER', \
     'test_addentry.usercard: server returned %s' % response['Status']
    assert response['Data']['Item-Count'] == '2', \
     'test_addentry.usercard: server returned wrong number of items'
    data_size = int(response['Data']['Total-Size'])
    conn.send_message({'Action': 'TRANSFER'})

    chunks = list()
    tempstr = conn.read()
    data_read = len(tempstr)
    chunks.append(tempstr)
    while data_read < data_size:
        tempstr = conn.read()
        data_read = data_read + len(tempstr)
        chunks.append(tempstr)

    assert data_read == data_size, 'test_orgcard.usercard: size mismatch'

    # Now that the data has been downloaded, we put it together and split it properly. We should
    # have two entries
    entries = ''.join(chunks).split('----- END USER ENTRY -----\r\n')
    if entries[-1] == '':
        entries.pop()

    # These entries are added to the database in init_server(). The insert operations are not
    # done here because the two org entries are needed for other tests, as well.
    assert len(
        entries
    ) == 2, "test_orgcard.usercard: server did not send the expected entry"
    assert entries[1] == '----- BEGIN USER ENTRY -----\r\n' + \
     second_user_entry.make_bytestring(-1).decode(), \
     "test_orgcard.usercard: entry didn't match"

    conn.send_message({'Action': "QUIT"})