In the research, an evaluation is conducted to assess the vulnerabilities of existing cryptographic algorithms commonly used in IoT devices against potential quantum attacks. With the advent of quantum computers, certain algorithms like RSA and ECC are known to [9][10][11]be susceptible to attacks based on quantum algorithms like Shor's algorithm. This evaluation highlights the need for exploring post-quantum solutions to secure IoT devices.
3.1 RSA Vulnerability
RSA (Rivest-Shamir-Adleman) is a widely used public-key encryption algorithm. Large composite numbers are used because factoring them into primes is challenging. Shor's algorithm, a quantum algorithm [12]13][14][15], can efficiently factor in large numbers by leveraging the properties of quantum computing.
The RSA algorithm involves the following steps:
1. Key Generation:
- Alice chooses 2 distinct prime numbers, p = 11 and q = 17.
- She computes n = p * q = 11 * 17 = 187.
- Alice also computes Euler's totient function, φ(n) = (p - 1) * (q - 1) = 10 * 16 = 160.
- Alice then picks a public exponent e that is coprime to φ(n) and such that 1< e < φ(n) Assume that e = 7.
- Alice computes the private exponent d as the modular multiplicative inverse of e modulo φ(n), such that (d * e) % φ(n) = 1. In this example, d = 23.
2. Encryption:
- Bob shares his public key (n, e) with Alice.
- Alice wants to use Bob's public key to encrypt the message M = 123.
- She applies the encryption formula: C = M^e % n = 123^7 % 187 = 47.
3. Decryption:
- • Bob, the receiver, decrypts the ciphertext using his private key (n, d).
- He applies the decryption formula: M = C^d % n = 47^23 % 187 = 123.
Assume an attacker comes in, Eve, has a quantum computer and applies Shor's algorithm to factor the modulus n = 187. Shor's algorithm can efficiently find the prime factors p and q.
As a result, Eve can compute p = 11 and q = 17. With this information, she can compute φ(n) = (p - 1) * (q - 1) = 10 * 16 = 160.
Using the same encryption and decryption formulas as before, Eve can intercept the ciphertext C = 47 and compute the plaintext M as M = C^d % n = 47^23 % 187 = 123.
Therefore, RSA encryption is susceptible to attacks by quantum computers running Shor's algorithm, compromising the security of the communication.
3.2 ECC Vulnerability
ECC (Elliptic Curve Cryptography) is another widely used public key encryption algorithm. Its key generation, encryption, and decryption are all based on the mathematical principles of elliptic curves. [16] ECC also faces the threat of being broken by Shor's algorithm.
In the context of ECC, let's consider the following example
1. Key Generation:
• Alice selects an elliptic curve and a curve base point.
• She also chooses a private key, d = 5, and determines her public key, Q = d * base_point.
2. Encryption:
- Bob shares his public key (the coordinates of the point on the curve) with Alice.
- Using Bob's public key, Alice wishes to encrypt the message M = 123.
- She calculates the point K = k * base_point using a secret number k that she selects at random.
- Alice computes the coordinates of the ciphertext by taking the x-coordinate of K and performing some additional operations.
3. Decryption:
- The shared secret value is calculated by Bob, the receiver, using the ciphertext and his private key.
- He retrieves the x-coordinate of the point K = d * K.
If an attacker, Eve, had a quantum computer running Shor's algorithm, she may calculate Bob's secret key by calculating the discrete logarithm, similar to how the RSA works. [19]Eve can decipher the ciphertext and get the plaintext message by using the secret key.
Two families of post quantum cryptography algorithms are investigated in the study to meet this demand. Hash based and lattice based systems are among these groups.
3.3 Lattice-based cryptography
A subset of post-quantum cryptography known as lattice-based cryptography depends on the difficulty of certain lattice-related mathematical issues. A lattice is an n-dimensional discrete grid-like structure that is created by an integer linearly combining a collection of linearly independent vectors[20]. Lattice-based encryption protects against quantum attacks by taking use of the computationally challenging nature of certain lattice problems[21].
1. Lattice:
- A lattice in n-dimensional space can be represented as L = {v1, v2, ..., vn}, where each vi is a linearly independent vector.
- A lattice can be visualized as a grid-like structure where the vectors vi form the basis of the lattice.
2. Lattice Problem: Shortest Vector Problem (SVP)
- A key issue in lattice-based encryption is the Shortest Vector Problem (SVP).
- The SVP searches a lattice L for the shortest non-zero vector, or the vector with the least Euclidean norm.
3. Lattice-Based Encryption:
- The difficulty of lattice issues serves as the foundation for lattice-based encryption techniques.[38]
- The plaintext message is converted into a lattice point during the encryption process, and a random noise vector is then added to obscure the message.
- A public key produced from the lattice basis is then used to encrypt the communication that has been obscured as a consequence.
- A secret key is used during the decryption process to unlock the ciphertext and reveal the original plaintext.
4. Lattice-Based Signature Schemes:
- Lattice-based signature methods provide secure [39]digital signatures by taking advantage of the difficulty of lattice issues.
- These schemes involve generating a hash of the message, applying a lattice-based transformation using a secret key, and producing a signature.
- By using the relevant public key transformation and [40] contrasting the calculated and received signatures, the signature may be confirmed.
5. Security of Lattice-Based Cryptography:
- It is assumed that lattice problems like the SVP are computationally challenging in order to maintain the security of lattice-based encryption.
- Both conventional and quantum computers are thought to be unable to tackle lattice issues.
- The difficulty of these issues offers a basis for developing quantum-resistant cryptography systems.
3.3.1 Algorithm
1. Key Generation:
• Produce a random matrix A of size n x m, where n denotes the lattice dimension and m the required dimension of the lattice basis.
• Apply the LLL algorithm to matrix A to perform lattice reduction to produce a reduced basis matrix B.
2. Encryption:
- Convert the plaintext message into a lattice point vector x.
- Generate a random noise vector e of length n.
- Compute the obfuscated message vector y = x + e.
- Encrypt the obfuscated message y using the reduced basis matrix B to obtain the ciphertext c = B * y.
3. Decryption:
- Decrypt the ciphertext c using the reduced basis matrix B to obtain the decrypted message vector y' = B^(-1) * c.
- Retrieve the original plaintext message x' by rounding each element of y' to the nearest integer.
4. Security:
- The difficulty of lattice issues like the SVP and the Learning With Errors (LWE) problem is essential to the security of lattice-based encryption.
- The LLL algorithm helps in lattice reduction, which reduces the basis of the lattice and enhances the security of the cryptographic scheme.
- The difficulty in solving lattice problems provides resistance against attacks by classical and quantum computers.
3.3.2 Code
import numpy as np
from scipy.linalg import qr
from scipy.linalg import svd
def lattice_reduction(L):
Q, R = qr(L)
U, S, Vt = svd(R)
R_star = np.dot(U, Vt)
return np.round(R_star).astype(int)
def encrypt(message, public_key):
lattice_point = np.array(message)
noise_vector = np.random.randint(low=-10, high=10, size=lattice_point.shape)
obfuscated_message = lattice_point + noise_vector
ciphertext = np.dot(obfuscated_message, public_key)
return ciphertext
def decrypt(ciphertext, secret_key):
obfuscated_message = np.dot(ciphertext, secret_key)
message = obfuscated_message.astype(int)
return message
# Example usage:
public_key = np.array([[2, 3], [4, 5]])
secret_key = lattice_reduction(public_key)
message = np.array([1, 2])
ciphertext = encrypt(message, public_key)
decrypted_message = decrypt(ciphertext, secret_key)
print("Decrypted Message:", decrypted_message)
In this code snippet, we use the LLL algorithm for lattice reduction. The LLL algorithm helps to reduce the basis of the lattice to make it more efficient for lattice-based cryptography.
The lattice_reduction() function takes a lattice basis as input and performs lattice reduction using the LLL algorithm. It returns the reduced basis of the lattice.
A plaintext message and a public key (lattice basis) are required parameters for the encrypt() function. It generates a random noise vector, adds the noise vector to the message as obfuscation, and then encrypts the obfuscated message using the public key.
A ciphertext and a secret key (a reduced lattice basis) are required inputs for the decode() function. By doing the dot product of the ciphertext and the secret key and then rounding the resultant vector, the original message may be recovered.
3.4 Hash Based Cryptography
Hash based cryptography is a cryptographic approach that relies on the properties of cryptographic hash functions for secure [22][23] communication and authentication. It involves the use of hash functions to create digital signatures, provide message integrity, and generate cryptographic keys.
3.4.1 Cryptographic Hash Function
A mathematical operation known as a cryptographic hash function converts an input message of arbitrary length into a fixed-size hash value or message digest. It is intended to be a one-way function, [24][25][26] such that it would be computationally impossible to carry out the opposite operation and recover the original message from the hash value. An equation for a cryptographic hash function looks like this:
H(M) = h
Where:
- H represents the hash function.
- M represents the input message.
- h represents the resulting hash value or message digest.
The hash function should have certain properties, including:
- Determinism: It should always generate the same hash value given the same input.
- Pre-image resistance: Determining the original message from the hash value should be computationally impossible.
- Collision resistance: It ought to be very rare that two separate inputs will result in the same hash value.
3.4.1.1 Algorithm
- Import the necessary libraries for cryptographic hash functions (e.g., hashlib).
- Define a function to calculate the hash value:
- Input: Message
- Output: Hash value
- Create a hash object using the desired cryptographic hash function (e.g., SHA-256) from the hashlib library.
- Convert the message to bytes using an appropriate character encoding (e.g., UTF-8).
- Using the update() function, add the message bytes to the hash object.
- Obtain the hash value by converting the final hash object to a hexadecimal representation using the hexdigest() method.
- Return the hash value.
3.4.1.2 Code
import hashlib
def calculate_hash(message):
# Create a SHA-256 hash object
hash_object = hashlib.sha256()
# Convert the message to bytes (hash functions operate on bytes)
message_bytes = message.encode('utf-8')
# Update the hash object with the message bytes
hash_object.update(message_bytes)
# Get the hexadecimal representation of the hash value
hash_value = hash_object.hexdigest()
return hash_value
# Example usage
message = "Hello, world!"
hash_value = calculate_hash(message)
print("Hash value:", hash_value)
In this code snippet, we first import the hashlib library, which provides the sha256() function for SHA-256 hashing. We define the calculate_hash() function that takes a message as input.
The calculate_hash() method first uses the hashlib.sha256() function to produce a sha256 hash object. In order to use a hash function, we must first encode the message using the encode() method using the 'utf-8' encoding. The bytes of the message are added to the hash object through the update() function.
Once we get the hash value, we use the hexdigest() function to convert it to hexadecimal and return that.
3.4.2 Message Digests and Digital Signatures
A message digest is a fixed-size output obtained by applying a cryptographic hash function to a message. It serves as a condensed representation of the original message[27][28][29]. The equation for calculating a message digest can be expressed as follows:
Digest = HashFunction(Message)
Where:
- Digest represents the resulting message digest.
- HashFunction denotes the chosen cryptographic hash function.
- Message represents the input message.
The most important things about message digests are that they are always the same size and that even a small change in the message will make the digest very different. So, they can be used to check the security of data and make sure it is correct.
Digital signatures provide authentication and integrity to digital documents or messages. They are generated using a combination of cryptographic hash functions and asymmetric key algorithms. The equation for generating a digital signature can be represented as follows:
Signature = Sign(HashFunction(Message), PrivateKey)
Where:
- Signature represents the resulting digital signature.
- Sign denotes the signing algorithm.
- HashFunction represents the cryptographic hash function.
- Message represents the input message.
- PrivateKey refers to the private key of the signer.
To verify the digital signature, the following equation is used:
Verified = Verify(HashFunction(Message), Signature, PublicKey)
Where:
- Verified is a boolean value indicating whether the signature is valid or not.
- Verify denotes the verification algorithm.
- HashFunction represents the cryptographic hash function.
- Message represents the input message.
- Signature represents the digital signature.
- PublicKey refers to the public key of the signer.
In the proof process, the same encryption hash function is used to make a new hash value for the message. Then, the signature is decrypted using the signer's public key[31][32]. If the generated hash value matches the decrypted signature, the signature is considered valid.
Digital signatures are non-repudiable, which means that the signer can't say they didn't make the signature, as it can be verified by any party having access to the public key. [36][37]They ensure that the message remains intact and unaltered during transit and guarantee the authenticity of the sender.
3.4.2.1 Algorithm
Step-by-step algorithm for generating a digital signature using a cryptographic algorithm like RSA
- Import the necessary libraries for cryptographic operations (e.g., cryptography).
- Generate or obtain the private key of the signer.
- Define a function to generate the digital signature:
- Input: Message, Private Key
- Output: Digital Signature
- Create a signer object using the private key and specify the desired padding scheme (e.g., PSS with a specific hash function).
- Update the signer object with the message to be signed.
- Finalize the signature generation process to obtain the digital signature.
- Return the digital signature.
3.4.2.2 Code
import cryptography.hazmat.primitives.asymmetric as asymmetric
import cryptography.hazmat.primitives.asymmetric.padding as padding
import cryptography.hazmat.primitives.hashes as hashes
function generate_signature(message, private_key):
signer = private_key.signer(padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH))
signer.update(message)
signature = signer.finalize()
return signature
# Example usage:
message = b"Hello, world!"
private_key = asymmetric.rsa.generate_private_key(public_exponent=65537, key_size=2048)
signature = generate_signature(message, private_key)
print("Digital Signature:", signature.hex())
In this code snippet, we import necessary modules from the cryptography library for generating digital signatures using RSA and SHA-256.
We define the generate_signature() function that takes a message and a private key as inputs. Inside the function, we create a signer object using the private key and specify the padding scheme as PSS with SHA-256. We then update the signer with the message and finalize the signature generation. The resulting signature is returned.
3.4.3 Merkle Tree Structures
A binary tree data structure called a Merkle tree, usually referred to as a hash tree, is widely used in computer science and cryptography. It bears Ralph Merkle's name since he originated the idea in 1979 [33]. Large data sets may be efficiently checked for consistency and integrity using the Merkle tree.
A Merkle tree is constructed by recursively hashing pairs of data until a single root hash, called the Merkle root, is obtained as shown in Figure 1[34][35]. The structure of a Merkle tree can be illustrated using a diagram and explained using equations:
Figure 1: Merkle Tree
- Hash(x) represents the hash function applied to data element x.
- Leaf_i represents the ith leaf node in the tree, where i ranges from 1 to n (the total number of leaf nodes).
- H(Leaf_i) represents the hash value of the ith leaf node.
- Internal nodes in the tree are obtained by hashing the concatenation of their child nodes' hash values.
- For example, Hash 1 = Hash(H(Leaf 1) + H(Leaf 2)) and Hash 2 = Hash(H(Leaf 3) + H(Leaf 4)).
- The Merkle root is the final hash value obtained by hashing the concatenation of the hash values of the two child nodes of the root.
- For example, Root Hash = Hash(Hash 1 + Hash 2).
3.4.3.1 Algorithm
Step by step algorithm to build a Merkle tree and calculate the Merkle root
1. Import the necessary libraries for cryptographic operations (e.g., hashlib).
2. Define a function to build the Merkle tree:
- Input: List of data elements
- Output: Merkle root
3. If the length of the data list is 1, return the single element as the Merkle root.
4. Create an empty list called next_level to store the next level of nodes in the tree.
5. Iterate through the data list in pairs:
- Take the left and right elements of each pair.
- Concatenate the left and right elements.
- Hash the concatenated string using a cryptographic hash function (e.g., SHA-256).
- Append the resulting hash value to the next_level list.
6. Recursively call the build_merkle_tree() function with the next_level list as the new input.
7. Repeat steps 3-6 until a single Merkle root is obtained.
8. Return the final Merkle root.
3.4.3.2 Code
import hashlib
function build_merkle_tree(data):
if len(data) == 1:
return data[0]
next_level = []
for i in range(0, len(data), 2):
left = data[i]
right = data[i+1] if i+1 < len(data) else data[i]
combined = left + right
hash_combined = hashlib.sha256(combined.encode()).hexdigest()
next_level.append(hash_combined)
return build_merkle_tree(next_level)
# Example usage:
data = ['Leaf 1', 'Leaf 2', 'Leaf 3', 'Leaf 4']
merkle_root = build_merkle_tree(data)
print("Merkle Root:", merkle_root)
In this algorithm, the build_merkle_tree() function is recursively called to construct the Merkle tree. The function takes a list of data elements as input and returns the Merkle root.
The algorithm utilizes the hashlib library in Python to perform the cryptographic hash function (SHA-256) on concatenated strings.
In this algorithm, a list data is created with four data elements ('Leaf 1', 'Leaf 2', 'Leaf 3', 'Leaf 4'). The build_merkle_tree() function is called with the data list, and the resulting Merkle root is stored in the merkle_root variable. Finally, the Merkle root is printed.