Designing Secure Software with Integrity in Mind

Author of the article: Rustem Galiev

IBM Senior DevOps Engineer & Integration Architect. Official DevOps mentor and coach at IBM

Today we'll look at designing secure software to ensure data integrity. How can we ensure that our software is designed in such a way that data is resistant to attack and not subject to change?

We've already looked at how hashing can be used to provide confidentiality, but it's also often used to check integrity. Hashing generates a unique value for any input and does so very quickly.

This makes it a strong form of verification that the message sent has not been tampered with.

Let's say we have a sender with a message. Before sending a message, the sender runs it through a hash function to obtain a hash value. This value should be generated based on this message only. The hash of any other message, even slightly different, will create a different hash value.

When the recipient receives the message, he also receives a hash value from the sender. Upon receiving the message, the recipient also generates a hash using the same message.

If the hash matches the one that was sent with the message, we know that nothing has been tampered with. This is how we use hashes to check integrity.

import hashlib

def calculate_hash(data):
    """Вычисление SHA-256 хеша для заданных данных"""
    return hashlib.sha256(data.encode()).hexdigest()

# Отправитель создает сообщение и его хеш
message = "This is a secure message."
hash_value = calculate_hash(message)
print(f"Original Message: {message}")
print(f"Hash Value: {hash_value}")

# Отправитель отправляет сообщение и хеш получателю

# Получатель получает сообщение и хеш
received_message = "This is a secure message."
received_hash_value = hash_value

# Получатель вычисляет хеш для полученного сообщения
calculated_hash_value = calculate_hash(received_message)
print(f"Received Message: {received_message}")
print(f"Calculated Hash Value: {calculated_hash_value}")

# Проверка целостности
if calculated_hash_value == received_hash_value:
    print("Integrity verified: The message was not tampered with.")
else:
    print("Integrity verification failed: The message was tampered with.")

Hashing only confirms the integrity of the file. It doesn't tell you who sent the file in the first place. To do this, we need a way to verify the identity of the sender (non-objection), which we can do using asymmetric encryption.

The developer hashes the source code to obtain a hash value that will verify the integrity of the file. They then package both the code and the hash value together and sign it with their private key.

This encrypts the package in such a way that only the developer's public key – the shared key – can decrypt it.

The package is then sent as a signed package, which verifies the integrity of the file and the identity of the developer. Let's see how it works.

To verify the package, after receiving the code, the recipient decrypts the signed package using the developer's public key.

Successful decryption using the public key proves that it was the developer who sent the package, since only the developer has the private key to encrypt or sign it.

The recipient then hashes the code to obtain a hash value. If it matches the hash value sent by the developer, the recipient knows that the file was not modified and it was the developer who sent it.

Code signing is often used in the context of the sender and recipient. Many modern systems also use it to ensure that data has not been modified before execution.

As an example, the iPhone operating system will only launch if it passes a series of code-signing tests to ensure that the OS has not been tampered with.

Another important concept is referential integrity. We see referential integrity most often in the case of files or data with relationships.

For example, if you delete a folder on your desktop, it makes sense that the files inside the folder should also be deleted.

In software, we often encounter this in databases where our data has certain relationships. We want to maintain the integrity of the database with no remaining records or inconsistent relationships.

In this case, we have three tables in the database. The first table is for users, the next for payments, and the last for addresses. The relationship is obvious to humans: payment information is associated with the user.

If we delete a user, we don't want any database entries left behind, so we need to make sure the payment method and address are also deleted.

This is an example of referential integrity, which helps maintain a high level of data integrity in a database.

Let's say business teams access a file on the system. When they gain access to a resource, the resource is then locked. If the audit team wants to access the same resource at the same time, they cannot change it because it is in a locked state.

This ensures that the audit team can only make changes after the business team has completed their changes. This guarantees data integrity.

However, you need to be careful with resource locking because if implemented poorly, it can become a vulnerability for hackers. Imagine a situation where a hacker simply blocks every resource from other users and causes a denial of service.

This is important to consider when designing the resource locking mechanism in your software.

import threading
import time

# Пример ресурса, который может быть заблокирован
class Resource:
    def __init__(self):
        self.lock = threading.Lock()  # Мьютекс для блокировки ресурса
        self.is_locked = False

    def access_resource(self, team_name):
        with self.lock:
            while self.is_locked:
                print(f"Resource is locked, {team_name} is waiting.")
                time.sleep(1)
            self.is_locked = True
            print(f"{team_name} has accessed the resource.")
            time.sleep(5)  # Предполагаемая работа с ресурсом
            self.is_locked = False
            print(f"{team_name} has finished and released the resource.")

# Создаем экземпляр ресурса
resource = Resource()

# Пример команд, которые пытаются получить доступ к ресурсу
def business_team():
    resource.access_resource("Business Team")

def audit_team():
    resource.access_resource("Audit Team")

# Создаем потоки для команд
thread_business = threading.Thread(target=business_team)
thread_audit = threading.Thread(target=audit_team)

# Запускаем потоки
thread_business.start()
thread_audit.start()

# Ждем завершения потоков
thread_business.join()
thread_audit.join()

print("All teams have finished accessing the resource.")

If you look at it, we have

Class Resource:

init: Initializes the mutex (self.lock) and the self.is_locked flag, indicating the locked state of the resource.

Method access_resource(team_name):

Uses the context manager with self.lock: to lock the mutex.

If self.is_locked == Truethe thread waits for the resource to be released (while self.is_locked: …).

After capturing a resource, a message about team access ({team_name} has accessed the resource.) is displayed.

It assumes working with a resource (represented by time.sleep(5)).

After the work is completed, the resource is released (self.is_locked = False) and a message about releasing the resource is displayed ({team_name} has finished and released the resource.).

Functions business_team() And audit_team():

Examples of commands that attempt to access a resource. They call the method resource.access_resource() indicating the command name.

Creating and running threads:

Two threads are created (thread_business and thread_audit), each of which calls the function of the corresponding command.

Threads are started using the method start().

Waiting for threads to complete:

The main thread (main program) waits for threads to complete using the method join().

Conclusion:

After all commands have completed, the message “All teams have finished accessing the resource.” is displayed.

This example demonstrates a basic implementation of the resource locking mechanism using a mutex in Python. In a real application, the locking mechanism may be more complex, including checking for resource availability before locking, using timestamps, etc.


You can learn more about secure software design in online courses from practicing industry experts. Details in the catalogue.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *