Chapter 7: Quantum Cryptography

This chapter covers arguably the most successful sub-field of quantum information processing, namely, quantum cryptography. Additionally, in this chapter, we will use Python as a tool for the hands-on implementation of both conventional cryptography and quantum cryptography.

In this chapter, we will showcase applications of quantum cryptography and Python implementations of certain quantum key distribution schemes. We will begin by implementing classical cryptographic primitives in Python. Furthermore, we will also learn how to implement quantum cryptographic primitives in Python.

In this chapter, we will cover the following main topics:

  • Introducing classical cryptography
  • Quantum cryptography
  • Post-quantum cryptography

By the end of this chapter, you should be able to do the following:

  • Implement classical cryptographic protocols such as Caesar's cipher and one-time pads in Python.
  • Implement quantum cryptographic protocols such as the BB84, the B92, and the E91 in Python.
  • Understand how post-quantum cryptography operates.

In the next section, we will cover the technical requirements for you to understand this chapter.

Technical requirements

The requirements for this chapter are the following:

  • A basic understanding of the Python programming language
  • Navigation of Google's Colab environment
  • Elementary (post-secondary) mathematics

The GitHub link for this chapter can be found here:

https://github.com/PacktPublishing/Hands-On-Quantum-Information-Processing-with-Python/tree/master/Chapter07

The next section will provide an introduction to classical cryptography.

Introducing classical cryptography

In this section, we will provide a basic introduction to classical cryptography. We will briefly go through the history of cryptography. Furthermore, we will explore cryptographic schemes such as the Diffie-Hellmann scheme. Finally, we will cover classical cryptographic primitives, such as random number generators.

Cryptography is the science and art of securing information in such a way that only the intended (legitimate) parties have access to such a communication. That is, cryptography makes it possible to ensure that illegitimate parties do not have access to the message that is being protected by the legitimate, communicating entities.

Cryptography is carried out by first converting an ordinary piece of information (referred to as plaintext) into an unintelligible string of characters (referred to as ciphertext). This conversion process is done so that the information transmitted from the transmitter (conventionally called Alice) is not intercepted by an eavesdropper (conventionally called Eve) on its way to the receiver (conventionally called Bob).

Once the plaintext has been converted to ciphertext, it can then be transmitted over the channel to Bob. In the channel, Eve can have access to such a ciphertext, but cannot make sense of such information (ciphertext), since it is unintelligible. On the receiver's side (Bob's side), the ciphertext is then converted back to plaintext.

From the details discussed previously, it can be observed that cryptography can be divided into two processes. These processes are encryption and decryption. The former (encryption) consists of the conversion of plaintext to ciphertext, using a cryptographic key. On the other hand, the latter (decryption) consists of the conversion of ciphertext to plaintext, with the help of a cryptographic key.

A schematic diagram of the cryptographic system (cryptosystem) is shown here:

Figure 7.1 – A schematic diagram of a cryptographic system

Figure 7.1 – A schematic diagram of a cryptographic system

The main goals of cryptography can be summarized as follows:

  • Confidentiality (secrecy): To ensure that information is rendered inaccessible to the illegitimate communicating parties. Thus, secrecy ensures that there is no unauthorized access to the information.
  • Data integrity: To ensure that information is not modified by illegitimate communicating parties during transmission.
  • Authentication: To guarantee that the sender of the information is who they claim to be. This, in turn, ensures that the only parties that can legitimately communicate are those that are verified as genuine.
  • Non-repudiation: To provide protection against one of the entities involved in a communication from denying having participated in such a communication.

Based on the nature of the cryptographic key being used, cryptography can be broadly divided into two branches, namely:

  • Symmetric cryptography (secret-key cryptography)
  • Asymmetric cryptography (public-key cryptography)

Symmetric cryptography uses a single cryptographic key that is shared by both Alice and Bob (recall that Alice and Bob are the legitimate communicating parties). In order to guarantee the protection of the information being shared between Alice and Bob, the cryptographic key used must be kept secret for as long as the information is kept secret.

Now that we have provided a brief description of symmetric cryptography, it is time to turn our attention to asymmetric cryptography. In this branch of cryptography, two separate but related cryptographic keys are used. One key, known as the private key, is kept secret, while the other key, known as the public key, is made public (and hence can be transmitted over an insecure but authenticated communication channel).

Having provided some background information on cryptography, it is now time to briefly explore the history of cryptography. Therefore, the next subsection will cover the history of cryptography.

A history of classical cryptography

Throughout history, cryptography has been used for various purposes, such as the following:

  • Diplomatic communications
  • Military operations
  • Commercial activities
  • Private communications
  • Religious applications

Earlier implementations of cryptography, which is known as classical cryptography, were primarily concerned with the substitution of characters in a message. One of the most prominent schemes in classical cryptography is Caesar's substitution cipher.

Caesar's cipher

The Caesar's cipher cryptographic scheme uses a cyclic alphabet and substitutes a character with another three places later in the alphabet. Thus, a character a will be substituted with a character d.

As an example, suppose you wanted to communicate the message YES using Caesar's cipher. So, the encrypted message would be:

  • Y = B
  • E = H
  • S = V

This is BHV.

The Python code for implementing Caesar's cipher is shown as follows:

message = "YES"

shift = 3

encryption = ""

for c in message:

    if c.isupper():

        c_unicode = ord(c)

        c_index = ord(c) - ord("A")

        new_index = (c_index + shift) % 26

        new_unicode = new_index + ord("A")

        new_character = chr(new_unicode)

        encryption = encryption + new_character

    else:

        encryption += c

print("Plain text:", message)

print("Encrypted text:", encryption)

The preceding Python code can be summarized as follows. First, the message that is to be encrypted using Caesar's cipher is provided. In this case, the message is YES. Then the shift of three steps (three letters, in a cyclic manner) is provided. Thereafter, each of the characters of the message are shifted using a shift of three. Finally, both the original message and the encrypted message are displayed.

Having covered Caesar's cipher, we will now turn our attention to the one-time pad. This will be covered next.

The one-time pad

Another key development in the history of classical cryptography was the development of the one-time pad (OTP). The OTP ensures that the encrypted message cannot be compromised, as long as the cryptographic key used to encrypt the message is used only once (hence, the one-time pad). Furthermore, the cryptographic key should have the same length (number of characters) as the message being encrypted.

The Python code for implementing the one-time pad scheme is shown as follows:

key = 5

message = "HELLO"

encrypt = ""

for i in range(len(message)):

    letter = ord(message[i])-65

    letter = (letter + key)%25

    letter +=65

    encrypt = encrypt + chr(letter)

print("Original message is:", message)

print("Encrypted message is:", encrypt)

The preceding Python code can be summarized as follows. First, the length of the key for the OTP scheme is set (in this case, it is set to 5). Then the message to be encrypted using OTP is provided. Furthermore, OTP is implemented using the given parameters, namely, the key length and the message. Finally, both the original message and the encrypted message are displayed.

So far, we have focused on the history of what is known as classical cryptography. Next, we will cover the history of what is now known as modern cryptography.

A history of modern cryptography

In the late 1970s, a new field of cryptography emerged. This would later be known as modern cryptography. Unlike classical cryptography, modern cryptography is more systematic, and takes a rigorous mathematical approach to data protection.

Modern cryptography is said to provide computational security. By this, we mean that the security of modern cryptography takes into account the computational capabilities of an eavesdropper, in order to guarantee the security of the cryptographic scheme.

In essence, modern cryptography provides a means of securing information by using mathematical techniques that are easier to compute for the legitimate communicating parties (due to the presence of a shared cryptographic key), but are computationally difficult to compute for illegitimate communicating parties (who do not have access to the cryptographic key).

One of the key contributions of modern cryptography was in the development of the cryptographic key exchange schemes. So far, we have just stated that these keys are required for both encryption and decryption processes. The natural question that arises then is how these keys are shared between Alice and Bob. Surely, merely transmitting them through the insecure channel would expose them to Eve! This challenge of exploring ways to securely exchange the cryptographic key between Alice and Bob through an insecure problem is known as the key distribution problem.

The advent of modern cryptography brought into sharp focus the key distribution problem discussed previously. Furthermore, in the late 1970s, Whitfield Diffie and Martin Hellman developed a cryptographic key exchange scheme that would later be known as the Diffie-Hellman key exchange protocol.

Diffie-Hellmann key exchange protocol

The Diffie-Hellman key exchange protocol, which is an example of asymmetric cryptography, can be summarized as follows. Alice and Bob agree on the key pair (p,g), where p is a large prime and g is a generator (base) such that:

Alice then chooses her secret integer a (which is her private key) and then computes:

,

which is her public key, and the mod function is the modulo function. Alice's public key is also known by Bob and Eve.

On the other end of the communication, Bob chooses his integer b (which is his private key). By now, Bob knows his private key (b) and Alice's public key (ga mod p). Furthermore, Bob can then perform the following computation:

It is not difficult to observe that.

Since Bob's public key is also known by Alice (and Eve for that matter), Alice can then compute.

Therefore, Alice and Bob have eventually generated a shared key, .

This is the same for both Alice and Bob. However, since Eve has no access to either a or b, she cannot compute the shared key given above. That way, Alice and Bob have managed to securely exchange a cryptographic key that can be used for the encryption of information.

A Python code for implementing the Diffie-Hellman scheme is shown as follows:

import math

p = input("Enter the shared prime number: ")

p = int(p)

g = input("Enter the shared base: ")

g = int(g)

a = input("Enter Alice's secret key: ")

a = int(a)

b = input("Enter Bob's secret key: ")

b = int(b)

AlicePublicKey = (g ** a ) % p

BobPublicKey = (g ** b) % p

AliceSecret = (BobPublicKey ** a) % p

BobSecret = (AlicePublicKey ** b) % p

print(AliceSecret)

print(BobSecret)

The preceding code snippet can be summarized as follows. First, the required parameters are entered. These parameters include p, g, a, and b discussed earlier. Since, in Python, the input is, by default, a string datatype, these inputs are then converted into integer datatypes using the int() function.

The next step in the previous code snippet is for both Alice and Bob to compute their public keys. Once this is done, Alice uses Bob's public key and her private key (a) to compute the shared secret key. On the other hand, Bob uses Alice's public key and his private key (b) to compute the shared secret key.

So far, we have provided a brief history of cryptography. We have introduced classical cryptography and modern cryptography. Furthermore, we have briefly discussed how the Diffie-Hellman key exchange scheme works. In the next subsection, we will cover the cryptographic primitives.

Cryptographic primitives

Cryptographic primitives are algorithms that can be used to build cryptographic protocols (schemes). That is, cryptographic primitives are the basic building blocks of cryptographic protocols, such as the Diffie-Hellman key exchange discussed earlier.

Examples of cryptographic primitives include the following:

  • Pseudorandom number generators (PRNGs)
  • Digital signatures
  • Hash functions
  • Secret sharing

Pseudorandom number generators

Randomness can be generated either from a true source of randomness or from a source that may seem random but, in reality, is not. The former option results in true randomness, while the other results in pseudorandomness.

The numbers generated from a pseudorandom generator are very close to the random number, even though they come from a source that is known to be deterministic as opposed to being truly random.

The Python code snippet for generating pseudorandomness using the random module is given as follows:

import random

random.seed(42)

x = []

y = []

for i in range(10):

    a = random.random()

    x.append(a)

print(x)

for i in range(5):

    b = random.randint(0,4)

    y.append(b)

print(y)

The preceding code snippet can be summarized as follows. First, the random module is imported into the workspace. Next, the seed is used in order to ensure the reproducibility of results. Then, two empty arrays, x and y, are created. The first array (x) is then used to store 10 pseudorandom floating-point numbers, while the second array (y) is used to store five pseudorandom integers.

We have provided a brief introduction to one of the primitives of cryptography, namely, PRNGs. We have also seen how PRNGs can be implemented in Python. Next, we discuss another primitive of cryptography, namely, the hash function.

Hash functions

Informally, hash functions are also referred to as one-way hash functions or message digests. A hash function is a computationally efficient mapping that maps data of arbitrary length to a fixed length.

The output of a hash function, which is of fixed length, is referred to as the hash value. Typically, the input to the hash function is a bit string of arbitrary length, while the output (hash value) is a bit string of fixed length.

Hash functions are normally used for cryptographic tasks such as ensuring data integrity. In such tasks, they are used in conjunction with another cryptographic primitive, namely, the digital signature.

Examples of hash functions include the following:

  • Message digest 5 (MD5)
  • Secure hashing algorithm (SHA)
  • BLAKE

A Python code snippet for implementing the MD5 hash function using the hashlib module is as follows:

import hashlib

AliceMessage = hashlib.md5()

Alice = "This is Alice"

Alice = Alice.encode(encoding='utf-8')

AliceMessage.update(Alice)

print("Alice's MD5 digest is: ", AliceMessage.hexdigest())

print("Alice's digest size is: ", AliceMessage.digest_size)

print("Alice's block size is: ", AliceMessage.block_size)

BobMessage = hashlib.md5()

Bob = "This is Bob"

Bob = Bob.encode(encoding='utf-8')

BobMessage.update(Bob)

print("Bob's MD5 digest is: ", BobMessage.hexdigest())

print("Bob's digest size is: ", BobMessage.digest_size)

print("Bob's block size is: ", BobMessage.block_size)

The preceding code snippet first imports the hashlib module from Python's standard library. The hashlib module contains a variety of hash functions. In the preceding code snippet, we focused on the MD5 hash function, and this is represented by the hashlib.md5() function. The next step was to provide the message that would be hashed, and then properly encode such a message.

The MD5 is not the only hash function that can be implemented in Python. Several others, including SHA and BLAKE, can also be implemented. The following code shows the Python implementation of the SHA scheme using the hashlib module:

import hashlib

AliceMessage = hashlib.sha3_512()

Alice = "This is Alice"

Alice = Alice.encode(encoding='utf-8')

AliceMessage.update(Alice)

print("Alice's SHA digest is: ", AliceMessage.hexdigest())

print("Alice's digest size is: ", AliceMessage.digest_size)

print("Alice's block size is: ", AliceMessage.block_size)

BobMessage = hashlib.sha3_512()

Bob = "This is Bob"

Bob = Bob.encode(encoding='utf-8')

BobMessage.update(Bob)

print("Bob's SHA digest is: ", BobMessage.hexdigest())

print("Bob's digest size is: ", BobMessage.digest_size)

print("Bob's block size is: ", BobMessage.block_size)

Just like the code snippet of the previous example (the MD5 code snippet), the preceding code snippet computes the hash function for both Alice and Bob's messages. The only difference is that instead of using the MD5 algorithm, the Python code provided above uses the Secure Hash Algorithm (SHA). Therefore, the function used in this case is the hashlib.sha3_512() function, which uses SHA-3 and has a 512-bit hash value.

So far, we have considered two cryptographic primitives, namely, PRNGs and hash functions. The final cryptographic primitive to consider in this chapter is secret sharing. This primitive is explored next.

Secret sharing

So far, we have considered cryptography from an angle where two communicating parties want to share a secret message. However, this communication can involve more than two communicating parties. In such cases, a suitable cryptographic primitive is secret sharing.

Secret sharing is a cryptographic primitive that is used to distribute a secret value among multiple communicating parties. This distribution is done by splitting/fragmenting the original message into various small fragments, and sending each fragment to each communicating party.

In order to recover the original message that was shared, the communicating parties would have to collaborate so that they can combine their fragments into the original message. The security of this cryptographic primitive lies in the fact that in order to recover the original message, the communicating parties would have to collaborate with other parties and recombine the various fragments of the message. This way, no individual can have access to the entire message without first collaborating with other communicating parties.

The secret sharing cryptographic primitive was independently proposed by Adi Shamir and George Blakley in 1979. Here, we focus only on the Shamir version of the secret sharing primitive.

The Shamir version of the secret sharing cryptographic primitive can be summarized as follows. Consider a secret message denoted by S, which is then divided into n parts, such that:

Then, a number k is chosen such that if

,

the message S could be successfully recovered. However, if the parts of the messages combined together are

then the original message S could not be successfully recovered.

We have covered the cryptographic primitives. Furthermore, we have provided a brief introduction to classical cryptography and modern cryptography. In the next section, we will cover quantum cryptography.

Quantum cryptography

We have learned in the previous section that modern cryptography makes assumptions about the computational capabilities of an eavesdropper. Although this cryptographic approach is still widely used, it faces a serious challenge in that its security cannot be proven theoretically.

As an alternative to modern cryptography, there is another approach to data protection. This approach provides an information-theoretic security guarantee instead of the computational security guarantee provided by modern cryptography. This approach is known as quantum cryptography.

In essence, quantum cryptography is a cryptographic approach that uses quantum mechanical concepts such as quantum entanglement, superposition, and the quantum no-cloning theorem (these concepts were discussed earlier in Chapter 2, Quantum States, Operations, and Measurements) in order to protect data.

A history of quantum cryptography

The history of quantum cryptography can be traced back to the 1970s, when Stephen Wiesner proposed a scheme that can be used to provide secure quantum money. The ideas from Wiesner's work were then expanded by Charles Bennett and Gilles Brassard in order to propose the first ever quantum key distribution (QKD) protocol in 1984. This QKD protocol would later be referred to as the BB84 protocol.

Another major breakthrough in quantum cryptography came in 1991, when Artur Ekert proposed another QKD protocol based on quantum entanglement. This QKD protocol proposed by Ekert would later be referred to as the E91 protocol.

Since its inception some decades back, quantum cryptography has enjoyed a massive growth over the years. It is now arguably the most successful field of quantum information processing.

In this subsection, we have just covered a brief history of quantum cryptography. In the next subsection, we will cover quantum cryptography primitives.

Quantum cryptography primitives

Earlier in this chapter, we explored the cryptographic primitives for modern cryptography. Here, we explore some of quantum cryptography primitives. The cryptographic primitives to be explored include the following:

  • Quantum random number generator (QRNG)
  • Quantum key distribution (QKD)

Quantum random number generator

Unlike the PRNG, the QRNG is truly random. Its randomness stems from an intrinsic randomness of performing measurements of quantum systems.

The Python code snippet for implementing a QRNG is as follows:

  1. First, import the necessary modules:

    import random

    from qiskit import *

    from qiskit.visualization import plot_histogram

    random.seed(42)

  2. Then, define the circuit for implementing the QRNG:

    circuit = QuantumCircuit(5,5)

    circuit.h(0)

    circuit.x(1)

    circuit.h(1)

    circuit.h(2)

    circuit.x(3)

    circuit.h(3)

    circuit.h(4)

    circuit.measure([0,1,2,3,4], [0,1,2,3,4])

    circuit.draw(output='text')

  3. Finally, simulate the circuit using the 'qasm_simulator':

    simulator = Aer.get_backend('qasm_simulator')

    result = execute(circuit, backend=simulator,

                     shots=1024).result()

    plot_histogram(result.get_counts(circuit))

The preceding code snippet shows how a five-qubit QRNG can be implemented using Python and qiskit. The five qubits are, by default, initialized to the quantum state, |0>. So, what we did was to apply the Hadamard gate in odd-numbered qubits while applying the X gate followed by the Hadamard gate for all the even-numbered qubits. The next step is then to apply the measurement to each of the five qubits. Finally, 'qasm_simulator' was used to simulate the results.

Now that we have covered the first quantum cryptography primitive, it is time to shift our focus to another primitive, namely, the key exchange primitive. This key exchange primitive in quantum settings is known as quantum key distribution.

Quantum key distribution

Quantum Key Distribution (QKD) is the key distribution scheme that employs quantum mechanical concepts in order to enable sharing of the cryptographic key between the legitimate communicating parties, Alice and Bob. The use of quantum mechanics ensures that the presence of an eavesdropper, Eve, could be revealed to Alice and Bob.

QKD uses two communication channels, namely, the quantum channel and the classical channel. The quantum channel is used for quantum communication, while the classical channel is used for classical post-processing. The following diagram shows the schematic diagram of a typical QKD scheme:

Figure 7.2 – A schematic diagram of a typical QKD scheme

Figure 7.2 – A schematic diagram of a typical QKD scheme

Typically, a QKD protocol should address some or all of the following issues: security, feasibility, and communication distance.

QKD protocols, which will be discussed later in this chapter, can be broadly divided into two categories, namely:

  • Prepare-and-measure QKD protocols. The BB84 discussed earlier is an example of this category.
  • Entanglement-based QKD protocols. The E91 protocol is an example of this category.

A typical QKD protocol is implemented through the phases summarized in the following diagram:

Figure 7.3 – A diagram of a typical QKD protocol implementation

Figure 7.3 – A diagram of a typical QKD protocol implementation

We have so far covered another quantum cryptographic primitive, and this primitive is referred to as the QKD. Having provided the background information on QKD, it is now time to focus on some of the QKD protocols. This will be discussed in the next subsection.

Quantum key distribution protocols

In this subsection, we will cover some of the QKD protocols. The protocols to be covered in this subsection include the following:

  • The BB84 QKD protocol
  • The B92 QKD protocol
  • The SARG04 QKD protocol
  • The E91 QKD protocol

The BB84 protocol

The BB84 protocol was the first QKD protocol to be invented by Bennett and Brassard. It uses four quantum states, which comprise any pair of conjugate states. The BB84 protocol functions as follows. Alice randomly chooses a quantum state and sends it over to Bob. On the receiving side, Bob then randomly chooses a measurement basis, and performs the measurement.

Once the quantum communication is done, Alice and Bob both use a classical channel to compare their values. Where they used similar bases for state preparation (Alice) and measurement (Bob), they keep bit values corresponding to such cases. Otherwise, they discard the values.

The Python code snippet for implementing the BB84 protocol is shown as follows:

  1. The first step involves importing the necessary modules:

    from qiskit import QuantumCircuit, execute, Aer

    from qiskit.visualization import *

    import matplotlib.pyplot as plt

    import numpy as np

    np.random.seed(42)

  2. The next step is to define the circuit for generating a random sequence of bit strings:

    circ = QuantumCircuit(1,1)

    circ.x(0)

    circ.barrier()

    circ.h(0)

    circ.barrier()

    circ.measure(0,0)

    circ.barrier()

    backend = Aer.get_backend('qasm_simulator')

  3. Alice then uses this defined random circuit to generate 128 random bits:

    result = execute(circ, backend, shots=128,

                     memory = True).result()

    bits_alice = [int(q) for q in result.get_memory()]

    print(bits_alice)

  4. Furthermore, Alice uses the random circuit to randomly choose the basis she is going to use in order to implement the BB84 protocol:

    result = execute(circ, backend, shots=128,

                     memory = True).result()

    basis_alice = [int(q) for q in result.get_memory()]

    print(basis_alice)

  5. The next step involves Bob randomly choosing his basis using the random circuit defined earlier:

    result = execute(circ, backend, shots=128,

                     memory = True).result()

    basis_bob = [int(q) for q in result.get_memory()]

    print(basis_bob)

  6. Now, Alice encodes the random bits she has generated into qubits and sends them over to Bob:

    bits_bob = []

    for i in range(128):

        circ_send = QuantumCircuit(1,1)

        if bits_alice[i]:

            circ_send.x(0)

        if basis_alice[i]:

            circ_send.h(0)

        if basis_bob[i]:

            circ_send.h(0)

        circ_send.measure(0,0)

        result = execute(circ_send, backend, shots = 1,

                         memory = True).result()

        bits_bob.append(int(result.get_memory()[0]))

    print(bits_bob)

  7. Bob then performs measurements and communicates to Alice regarding the bases he used to perform measurements. Eventually, if Bob's bases match with Alice's, then they generate and agree on a secret key:

    key = []

    for i in range(128):

        if basis_alice[i] == basis_bob[i]:

            key.append(bits_bob[i])

    print("Key length", len(key))

    print(key)

The preceding Python code for the implementation of the BB84 QKD protocol can be summarized as follows. First, the modules necessary for the implementation of the BB84 QKD protocol are imported. The next step involves the implementation of the BB84 protocol. Finally, Alice and Bob compare their bit strings, and retain those strings where their use of bases corresponds and discard the rest. The retained bit strings are then used as a secret key generated using the BB84 protocol.

The B92 protocol

The B92 QKD protocol is the simpler version of the BB84 protocol. It was proposed by Charles Bennett in 1992.

Instead of using four quantum states, like the BB84 protocol, it only uses two non-orthogonal states. The B92 QKD protocol is based on the principle that two quantum states that are not orthogonal are sufficient to detect the presence of an eavesdropper.

In order to realize the B92 protocol, Alice randomly prepares a quantum state in either of the two non-orthogonal states and sends it over to Bob. On the receiving end, Bob then performs the measurement and records the time slots where he received inconclusive results and where he measured the results with certainty. He then communicates this information over the classical channel to Alice. They then retain bit values for time slots where the measurements were done with certainty, and discard bit values for cases where the measurements were inconclusive.

The following steps show the implementation of the B92 protocol using Python:

  1. The first step entails importing the necessary modules:

    from qiskit import QuantumCircuit, execute, Aer

    from qiskit.visualization import *

    import matplotlib.pyplot as plt

    import numpy as np

    np.random.seed(42)

  2. Then, the circuit for generating a random string of bits (128 bits) is generated by Alice:

    circ = QuantumCircuit(1,1)

    circ.x(0)

    circ.barrier()

    circ.h(0)

    circ.barrier()

    circ.measure(0,0)

    circ.barrier()

    circ.draw(output='text')

    n = 128

    backend = Aer.get_backend('qasm_simulator')

    result = execute(circ, backend, shots=n,

                     memory = True).result()

    bits_alice = [int(q) for q in result.get_memory()]

    print(bits_alice)

  3. The next step involves Bob choosing at random the bases he will use to perform measurements:

    result = execute(circ, backend, shots=n,

                     memory = True).result()

    basis_bob = [int(q) for q in result.get_memory()]

    print(basis_bob)

    bits_bob = []

    for i in range(n):

        circ_send = QuantumCircuit(1,1)

        if bits_alice[i] == 0:

            circ_send.id(0)

        if bits_alice[i] == 1:

            circ_send.h(0)

        else:

            circ_send.id(0)

        circ_send.measure(0,0)

        result = execute(circ_send, backend, shots = 1,

                         memory = True).result()

        bits_bob.append(int(result.get_memory()[0]))

    print(bits_bob)

  4. Finally, both Alice and Bob communicate to generate and agree on the secret key generated:

    key = []

    for i in range(n):

        if bits_alice[i] == bits_bob[i]:

            key.append(bits_bob[i])

    print("Key length is:", len(key))

    print("The secret Key is:", key)

The preceding code snippet can be summarized as follows. First, as is always the case, the necessary modules are imported. The next step involves Alice's random generation of her bits to transfer to Bob. Based on the value of the bits generated, Alice can perform two tasks. If the generated bit value is '0', Alice does not change the basis (applies the identity gate). On the other hand, if the generated bit is '1', Alice changes the basis (applies the Hadamard gate).

On Bob's side, Bob randomly chooses the basis to use for measurement. Then, both Alice and Bob compare the bases they used. If the bases correspond, they keep the bit value corresponding to that. Otherwise, they discard the bit value.

The SARG04 QKD protocol

The SARG04 QKD protocol was invented by Scarani, Acin, Ribordy, and Gisin in 2004. This protocol is an improvement on the BB84 protocol discussed earlier. This protocol is intended to be robust even when Alice uses a coherent light source (instead of a single-photon light source) for the preparation of quantum states.

Just like the BB84 protocol, the SARG04 protocol is also the four-state quantum key distribution protocol. The key difference between the two protocols lies in the classical post-processing.

So far, we have covered three QKD protocols, namely, the BB84, the B92, and the SARG04. All these QKD protocols are examples of the prepare-and-measure category of QKD schemes. Next, we will discuss another protocol, namely, the E91 protocol. Unlike the previous three QKD protocols, the E91 protocol is an example of the entanglement-based category of the QKD schemes.

The E91 QKD protocol

As already stated, the E91 protocol uses quantum entanglement as a quantum resource in order to secure the data. Additionally, this protocol was developed by Ekert, building on the ideas proposed by David Deutsch (Deutsch was mentioned earlier in this book, in Chapter 5, Quantum Algorithms).

The E91 QKD protocol operates as follows. First, both Alice and Bob share EPR pairs. Then they perform measurements on the parts of the pairs that are on their side. Since entanglement states are correlated, any measurement of the EPR pairs by either Alice or Bob would result in a correlated state on the side of the other communicating party (Bob or Alice).

The following steps show the implementation of the E91 protocol using Python:

  1. First, import the modules necessary for the implementation of the E91 protocol:

    from qiskit import *

    import numpy as np

    import matplotlib.pyplot as plt

    np.random.seed(42)

  2. Then, both Alice and Bob choose their bases to use:

    A = [0, np.pi/8, np.pi/4]

    B = [0, np.pi/8, -1*np.pi/8]

    basesA = []

    basesB = []

    output = []

  3. The next step involves defining the circuit to be used by Alice and Bob in order to generate a 100-bit random key:

    for i in range(100):

        circ = QuantumCircuit(2, 2)

        circ.h(0)

        circ.cx(0,1)

        Ta = np.random.choice(A)

        Tb = np.random.choice(B)

        circ.rz(Ta, 0)

        circ.rz(Tb, 1)

        circ.measure([0, 1], [0, 1])

  4. Then, simulate the circuit using the 'qasm_simulator':

        backend = Aer.get_backend('qasm_simulator')

        result = execute(circ, backend, shots=1,

                         memory=True).result()

    Finally, generate and display the random key.

        value = result.get_memory()

        output.append(value)

    print("The output is:", output)

The preceding Python code for the implementation of the E91 QKD protocol can be summarized as follows. The first step involves importing the necessary modules. This is then followed by the construction of the circuit for entanglement generation. Now that both Alice and Bob share an entangled state, the next step is for Alice to randomly and uniformly choose a measurement axis for each or her qubits. She does so by randomly and uniformly choosing to rotate her state by any of the angles 0, π/8, or π/4. She then performs a measurement on her qubit.

Bob, on the other hand, chooses his measurement axis by randomly and uniformly choosing to rotate his state by any of the angles 0, π/8, or -π/8. Just like Alice, Bob also performs a measurement on his qubit.

The final step in the implementation of the E91 QKD protocol from the preceding code involves simulating the circuit used for this implementation, using the 'qasm_simulator'. The results obtained are then displayed as a list of pairs of Alice's and Bob's measurement results.

We have provided a brief discussion of the QKD protocol. In the next section, we move beyond quantum cryptography. We explore the field of study that aims to develop non-quantum cryptographic schemes that are resistant to quantum computers. This field of study is known as post-quantum cryptography.

Post-quantum cryptography

Post-quantum cryptography is concerned with the development of cryptographic algorithms that are believed to be resistant to code breaking by quantum computers. Currently, most modern cryptography algorithms are known to be vulnerable to attacks using quantum computers.

Post-quantum cryptography makes use of the some of the mathematical concepts in order to design cryptographic systems that appear to be difficult to break, even for a cryptanalyst with access to a powerful quantum computer. Like modern cryptography systems discussed earlier, post-quantum cryptography systems provide computational security instead of the information-theoretic security provided by quantum cryptography.

Post-quantum cryptography can be implemented using any of the following approaches/classes:

  • Lattice-based cryptography, which can be used to develop digital signatures and key exchange cryptographic schemes
  • Multivariate quadratic equations cryptography, which is typically used to develop digital signature cryptographic schemes
  • Hash-based cryptography, which is typically used to develop digital signatures
  • Code-based cryptography, which is typically used to develop key exchange cryptographic schemes
  • Supersingular elliptic curve isogeny cryptography, which can be used to develop encryption schemes

Now that we have provided a brief introduction to post-quantum cryptography, the next subsections will cover some of the post-quantum cryptographic technics. In the next subsection, we will explore a lattice-based key exchange technique called the NewHope key exchange scheme.

The NewHope key exchange scheme

One of the best examples of post-quantum cryptographic tools is the NewHope key exchange scheme. This scheme uses the lattice-based approach. As a lattice-based approach, the NewHope cryptographic technique offers resistance to all known quantum algorithms. Furthermore, the NewHope key exchange technique is one of the fastest and, hence, most efficient lattice-based techniques.

The Python implementation of the NewHope key exchange scheme is given in the following code snippet:

from pynewhope import newhope

import numpy as np

np.random.seed(42)

ka, ma = newhope.keygen()

skb, mb = newhope.sharedB(ma)

ska = newhope.sharedA(mb, ka)

if ska == skb:

    print(" Successful key exchange! Keys match.")

else:

    print(" Error! Keys do not match.")

print("The shared key is:", ska)

In the preceding code snippet, the modules necessary to implement the NewHope key exchange scheme are imported. This is followed by Alice generating a random key and the message she intends to send to Bob. Bob then receives the message from Alice and responds to her with his own message. The two communicating parties then use the information communicated to generate the secret key. Finally, they verify that indeed their keys match. Finally, the shared key is displayed.

Having explored the NewHope key exchange technique in this subsection, the next subsection will cover the hash-based, post-quantum cryptographic scheme known as SPHINCS+.

The SPHINCS+ digital signature scheme

The SPHINCS+ cryptographic technique is an example of the hash-based approach to post-quantum cryptography. Additionally, it offers long-term security even against attackers equipped with quantum computers.

SPHINCS+ can only be used for developing quantum-resistant digital signatures. That is, it cannot be used for developing key exchange schemes.

The SPHINCS+ digital signature scheme can be implemented using either of the three hash functions, namely, the SHA-256 hash function, the SHA-2 hash function, or the Haraka short-input hash function.

The Python code snippet for implementing the SPHINCS+ digital signature scheme is given as follows:

  1. The first step involves importing the necessary modules:

    import pyspx.shake256_128s as sphincs

    import os, binascii

    import numpy as np

    np.random.seed(42)

  2. This step is then followed by the generation of both the private and public keys:

    # Key generation: private + public key

    seed = os.urandom(sphincs.crypto_sign_SEEDBYTES)

    public_key, secret_key = sphincs.generate_keypair(seed)

  3. Next, the message is provided, and its corresponding digital signature generated and verified:

    message = b'Hello World'

    signature = sphincs.sign(message, secret_key)

    valid = sphincs.verify(message, signature, public_key)

    message = b'Hello World'

    valid = sphincs.verify(message, signature, public_key)

    print("Tampered message:", message)

    print("Tampered signature valid?", valid)

    message = b'Bye World'

    valid = sphincs.verify(message, signature, public_key)

    print("Tampered message:", message)

    print("Tampered signature valid?", valid)

The preceding Python code can be summarized as follows. First, the necessary modules are imported. This is then followed by the provision and digital signature of the message. Next, verification is performed to confirm that the message was not tampered with.

We have now completed our coverage of post-quantum cryptography. The following section will summarize this chapter.

Summary

In this chapter, we have explored the field of quantum information processing known as quantum cryptography. Furthermore, we have discussed the use of Python for implementing cryptographic schemes, both for non-quantum and quantum cryptography. Additionally, we have covered the field of cryptography that is believed to be resistant to hacking by quantum computers. This field of cryptography is known as post-quantum cryptography.

Furthermore, in this chapter, you are expected to have learned more about some of the concepts in classical, modern, and quantum cryptography. Additionally, you are expected to have acquired hands-on skills in terms of how to implement cryptography using Python.

The skills acquired in this chapter can be applied in cybersecurity, where such skills can be used for the real-life design and engineering of cryptographic systems.

In the next chapter, we will explore another field of quantum information processing – quantum machine learning.

Further reading

  • Schneier, Bruce. Applied Cryptography: Protocols, Algorithms, and Source Code in C. John Wiley and sons, 2007.
  • Mafu, Mhlambululi, and Senekane, Makhamisa. Security of Quantum Key Distribution Protocols. In Advanced Technologies of Quantum Key Distribution, pp. 3-15, IntechOpen, 2018.
  • Bernstein, Daniel J. Introduction to Post-Quantum Cryptography. In Post-quantum cryptography, pp. 1-14. Springer, Berlin, Heidelberg, 2009.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.198.49