Ejemplo n.º 1
0
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()
Ejemplo n.º 2
0
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()
Ejemplo n.º 3
0
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)
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
0
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)
Ejemplo n.º 6
0
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)
Ejemplo n.º 7
0
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)
Ejemplo n.º 8
0
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)
Ejemplo n.º 9
0
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)
Ejemplo n.º 10
0
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()
Ejemplo n.º 11
0
def status():
    return_data = {'active': server_active, 'identity': SERVER_IDENTITY}
    rv = KeyExReturn.OK(json.dumps(return_data))
    return rv()
Ejemplo n.º 12
0
def handle_bad_json(error):
    logging.warning("Malformed JSON data received")
    return KeyExReturn.BadJSON(error.description)()
Ejemplo n.º 13
0
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)()