def access_keys(): access_key_data = request.get_json() identity = access_key_data['identity'] csr_data = base64.decodebytes(access_key_data['csr'].encode()) signature = base64.decodebytes(access_key_data['signature'].encode()) key_string = database_functions.get_key(DATABASE_FILE, identity) # ...and we need to restore the key to the full PEM format that the RSA functions are expecting. restored_key = KeyEx_helpers.fixkey(key_string) # Now we can import that restored key string into a full RSA key object, get the SAH256 hash for the # message (the device ID), and create a verifier object to check the signature... pbkey = serialization.load_pem_public_key(restored_key.encode(), backend=default_backend()) # Validate that the signature is valid for the csr_data signed by this device... try: pbkey.verify( signature, csr_data, padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256()) # We're okay – so sign it... with open(CA_KEY, 'rb') as f: ca_key_data = f.read() ca_key = serialization.load_pem_private_key(ca_key_data, b'rabbit', default_backend()) csr = x509.load_pem_x509_csr(csr_data, default_backend()) with open(CA_CRT, 'rb') as f: ca_crt_data = f.read() ca_crt = x509.load_pem_x509_certificate(ca_crt_data, default_backend()) if isinstance(csr.signature_hash_algorithm, hashes.SHA256): cert = x509.CertificateBuilder().subject_name(csr.subject).issuer_name(ca_crt.issuer).public_key\ (csr.public_key()).serial_number(x509.random_serial_number()).not_valid_before\ (datetime.datetime.utcnow()).not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365))\ .sign(ca_key, hashes.SHA256(), default_backend()) return_data = { 'certificate': base64.encodebytes( cert.public_bytes(serialization.Encoding.PEM)).decode(), 'CA_certificate': base64.encodebytes(ca_crt_data).decode() } rv = KeyExReturn.OK(json.dumps(return_data)) return rv() except (InvalidSignature, ValueError, TypeError) as e: rv = KeyExReturn.CSRVerificationError() return rv()
def validate(): validation_data = request.get_json() identity = validation_data['identity'] # Because the signatures are composed of arbitrary byte-streams we need to base64 encode them before trying to put # them into a JSON object - and so we must reverse that process here... signature = base64.decodebytes(validation_data['signature'].encode()) # Next we need to get the key string associated with the public key for the device that's claiming to have the # specified identity... key_string = database_functions.get_key(DATABASE_FILE, identity) # ...and we need to restore the key to the full PEM format that the RSA functions are expecting. restored_key = KeyEx_helpers.fixkey(key_string) # Now we can import that restored key string into a full RSA key object, get the SAH256 hash for the # message (the device ID), and create a verifier object to check the signature... pbkey = serialization.load_pem_public_key(restored_key.encode(), backend=default_backend()) # If the verifier passes the device key we have is a valid public key for the private key that the device is using try: pbkey.verify( signature, identity.encode(), padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256()) # If we get here without throwing an exception then the signature was valid: so we'll return our own identity, # signed with our private key with open(SERVER_PRIVATE_KEY_FILE, "rb") as key_file: private_key = serialization.load_pem_private_key( key_file.read(), password=None, backend=default_backend()) signature = private_key.sign( SERVER_IDENTITY.encode(), padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256()) # Note that as before - we must use base64 encoding before we can serialize this into JSON. return_data = { 'identity': SERVER_IDENTITY, 'signature': base64.encodebytes(signature).decode() } # Lastly we return the data, with a HTTP 200 rv = KeyExReturn.OK(json.dumps(return_data)) return rv() # If we threw an exception then the key didn't validate - so we must return a signature validation error (HTTP 400) except (InvalidSignature, ValueError, TypeError) as e: rv = KeyExReturn.SignatureVerificationError() return rv()
def test_http_success_return(): # Test KeyExReturn.Success() data = "{'Test':True, 'Name':'Test'}" okay = KeyExReturn.Success(data) status_code = 201 assert okay.status_code() == status_code assert okay.message() == data assert okay() == (data, status_code)
def test_http_database_connection_error(): # Test KeyExReturn.DatabaseConnectionError() status_code = 500 data = "An internal database error has occurred..." db = KeyExReturn.DatabaseConnectionError() assert db.status_code() == status_code assert db.message() == data assert db() == (data, status_code)
def test_http_forbidden(): # Test KeyExReturn.Forbidden() status_code = 403 data = "Server is inactive and not accepting new registration requests." forbidden = KeyExReturn.Forbidden() assert forbidden.status_code() == status_code assert forbidden.message() == data assert forbidden() == (data, status_code)
def test_http_database_error(): # Test KeyExReturn.DatabaseError() status_code = 500 text = "Something bad happened..." data = "A database error occurred - {}".format(text) db = KeyExReturn.DatabaseError(text) assert db.status_code() == status_code assert db.message() == data assert db() == (data, status_code)
def test_http_bad_json(): # Test KeyExReturn.BadJSON() status_code = 400 text = "Something is wrong with the data..." data = "Malformed JSON data received:\n{}".format(text) bad = KeyExReturn.BadJSON(text) assert bad.status_code() == status_code assert bad.message() == data assert bad() == (data, status_code)
def test_http_missing_json(): # Test KeyExReturn.MissingJSON() status_code = 400 json_data = '{"identity":"12345", "key":"1234567"}' data = "Missing or invalid values in JSON data received:\n{}".format( json_data) missing = KeyExReturn.MissingJSON(json_data) assert missing.status_code() == status_code assert missing.message() == data assert missing() == (data, status_code)
def test_http_non_unique_key_return(): # Test KeyExReturn.NonUniqueKey() dev_id = "ABC" data = "Device ID {} already exists; and could not be added.".format( dev_id) status_code = 400 non_unique = KeyExReturn.NonUniqueKey(dev_id) assert non_unique.status_code() == status_code assert non_unique.message() == data assert non_unique() == (data, status_code)
def get_pub_key(identity): # Given that we only need to pass one value – we won't use JSON – but rather use a simple # argument...e.g. .../get_key/a3ca9020-b2dc-4d4d-bbf9-42320c7730a0 # Get the key - if we have it... key_string = database_functions.get_key(DATABASE_FILE, identity) if key_string is None: # We don't have a key for this identity... rv = KeyExReturn.IDNotFound() else: # We have the key - so now we need to restore the key to the full PEM format # that RSA functions will expecting... restored_key = KeyEx_helpers.fixkey(key_string) # And to return this complete key to the C2 server we need to first Base64 encode it. encoded_key = base64.encodebytes(restored_key.encode()) rv = KeyExReturn.OK(encoded_key) return rv()
def status(): return_data = {'active': server_active, 'identity': SERVER_IDENTITY} rv = KeyExReturn.OK(json.dumps(return_data)) return rv()
def handle_bad_json(error): logging.warning("Malformed JSON data received") return KeyExReturn.BadJSON(error.description)()
def register(): registration_data = request.get_json() if KeyEx_helpers.check_valid_json(registration_data): # Having checked the validity we can safely access the JSON data by key name... identity = registration_data['identity'] pub_key = registration_data['key'] dev_type = registration_data['type'] if server_active: # The store_data function will check the device doesn't already exist, and then create a record for it, # and store it's key & type... # it will return false if there's an error – and the error type will be in error... db_ret = database_functions.store_data(DATABASE_FILE, identity, pub_key, dev_type) if db_ret.success(): if db_ret.type() == "Success": # All is good – so return the broker details, and the server's public key... key = KeyEx_helpers.keyfile(SERVER_PUBLIC_KEY_FILE) return_data = {'broker': MQTT_BROKER, 'key': key} rv = KeyExReturn.Success(json.dumps(return_data)) logging.info("Device {} added.".format(identity)) return rv() else: # If we get here - something went wrong: as we'll've returned True in success # but have the wrong return type... # This is another one of the "can't happen" errors unless we've screwed up. logging.error("Database return type invalid... {}".format(db_ret.message())) return KeyExReturn.DatabaseError("Internal Error")() else: # We couldn't add the data to the database... So let's find out why... if db_ret.type() == 'NonUnique': # Most likely is that we're trying to add a Device ID we already have... rv = KeyExReturn.NonUniqueKey(identity) logging.warning(rv.message()) return rv() elif db_ret.type() == 'NotConnected': # We couldn't connect to the database... # This is the worst case: so log as an error... logging.error("Could not connect to the database... {}".format(db_ret.message())) # Since this can't be caused by the user; there's nothing to report back beyond the generic message return KeyExReturn.DatabaseConnectionError()() else: # Something else went wrong with the database - so we'll return a generic database error # This has a status value of 500... # Together with any message from the database (this should never happen). return KeyExReturn.DatabaseError(db_ret.message())() else: # if server is inactive... # Note: the odd syntax here is because we initialize and then call the KeyExReturn.Forbidden object logging.warning("Device registration from {}, attempted when server was inactive.".format(identity)) return KeyExReturn.Forbidden()() else: # if invalid JSON... # We'll generate a list of the missing keys... missing = KeyEx_helpers.missing_json(registration_data) # ...and log that list as a warning. logging.warning("JSON data received had missing values: {}".format(missing)) # We'll also create a dictionary we can return showing the missing values. processed_data = {} for d in missing: processed_data.update({d: ''}) # Again we'll use the custom class - with the same syntax as the Forbidden case... return KeyExReturn.MissingJSON(processed_data)()